docyard 0.8.0 → 0.9.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 +24 -1
- data/lib/docyard/components/aliases.rb +12 -0
- data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
- data/lib/docyard/components/processors/accordion_processor.rb +81 -0
- data/lib/docyard/components/processors/badge_processor.rb +72 -0
- data/lib/docyard/components/processors/callout_processor.rb +8 -2
- data/lib/docyard/components/processors/cards_processor.rb +100 -0
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +23 -2
- data/lib/docyard/components/processors/code_block_processor.rb +6 -0
- data/lib/docyard/components/processors/code_group_processor.rb +198 -0
- data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +6 -1
- data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
- data/lib/docyard/components/processors/file_tree_processor.rb +151 -0
- data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
- data/lib/docyard/components/processors/include_processor.rb +86 -0
- data/lib/docyard/components/processors/steps_processor.rb +89 -0
- data/lib/docyard/components/processors/tabs_processor.rb +9 -1
- data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
- data/lib/docyard/components/processors/video_embed_processor.rb +196 -0
- data/lib/docyard/components/support/code_group/html_builder.rb +122 -0
- data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
- data/lib/docyard/config/branding_resolver.rb +30 -35
- data/lib/docyard/config/logo_detector.rb +39 -0
- data/lib/docyard/config.rb +6 -1
- data/lib/docyard/navigation/sidebar/file_resolver.rb +16 -4
- data/lib/docyard/navigation/sidebar/item.rb +6 -1
- data/lib/docyard/navigation/sidebar/metadata_extractor.rb +4 -2
- data/lib/docyard/navigation/sidebar/metadata_reader.rb +8 -4
- data/lib/docyard/navigation/sidebar/renderer.rb +6 -2
- data/lib/docyard/navigation/sidebar/tree_builder.rb +2 -1
- data/lib/docyard/rendering/icons/phosphor.rb +3 -0
- data/lib/docyard/rendering/markdown.rb +24 -1
- data/lib/docyard/rendering/renderer.rb +2 -1
- data/lib/docyard/server/asset_handler.rb +1 -0
- data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
- data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
- data/lib/docyard/templates/assets/css/components/badges.css +47 -0
- data/lib/docyard/templates/assets/css/components/banner.css +202 -0
- data/lib/docyard/templates/assets/css/components/cards.css +100 -0
- data/lib/docyard/templates/assets/css/components/code-block.css +10 -0
- data/lib/docyard/templates/assets/css/components/code-group.css +281 -0
- data/lib/docyard/templates/assets/css/components/figure.css +22 -0
- data/lib/docyard/templates/assets/css/components/file-tree.css +124 -0
- data/lib/docyard/templates/assets/css/components/heading-anchor.css +21 -13
- data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
- data/lib/docyard/templates/assets/css/components/navigation.css +7 -0
- data/lib/docyard/templates/assets/css/components/prev-next.css +9 -18
- data/lib/docyard/templates/assets/css/components/steps.css +122 -0
- data/lib/docyard/templates/assets/css/components/tabs.css +1 -1
- data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
- data/lib/docyard/templates/assets/css/components/video.css +41 -0
- data/lib/docyard/templates/assets/css/markdown.css +5 -3
- data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
- data/lib/docyard/templates/assets/js/components/banner.js +81 -0
- data/lib/docyard/templates/assets/js/components/code-group.js +283 -0
- data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
- data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
- data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
- data/lib/docyard/templates/layouts/default.html.erb +1 -0
- data/lib/docyard/templates/layouts/splash.html.erb +1 -0
- data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
- data/lib/docyard/templates/partials/_banner.html.erb +27 -0
- data/lib/docyard/templates/partials/_card.html.erb +23 -0
- data/lib/docyard/templates/partials/_nav_group.html.erb +6 -0
- data/lib/docyard/templates/partials/_nav_leaf.html.erb +3 -0
- data/lib/docyard/templates/partials/_step.html.erb +14 -0
- data/lib/docyard/version.rb +1 -1
- metadata +38 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base_processor"
|
|
4
|
+
require_relative "../support/code_block/feature_extractor"
|
|
5
|
+
require_relative "../support/code_block/line_wrapper"
|
|
6
|
+
require_relative "../support/code_group/html_builder"
|
|
7
|
+
require_relative "../support/markdown_code_block_helper"
|
|
8
|
+
require_relative "../../rendering/icons"
|
|
9
|
+
require_relative "../../rendering/renderer"
|
|
10
|
+
require "securerandom"
|
|
11
|
+
require "kramdown"
|
|
12
|
+
require "kramdown-parser-gfm"
|
|
13
|
+
require "cgi"
|
|
14
|
+
|
|
15
|
+
module Docyard
|
|
16
|
+
module Components
|
|
17
|
+
module Processors
|
|
18
|
+
class CodeGroupProcessor < BaseProcessor
|
|
19
|
+
include Utils::HtmlHelpers
|
|
20
|
+
include Support::MarkdownCodeBlockHelper
|
|
21
|
+
|
|
22
|
+
self.priority = 12
|
|
23
|
+
|
|
24
|
+
CODE_GROUP_PATTERN = /^:::[ \t]*code-group[ \t]*\n(.*?)^:::[ \t]*$/m
|
|
25
|
+
CODE_BLOCK_PATTERN = /```(\w*)\s*\[([^\]]+)\]([^\n]*)\n(.*?)```/m
|
|
26
|
+
|
|
27
|
+
CodeBlockFeatureExtractor = Support::CodeBlock::FeatureExtractor
|
|
28
|
+
CodeBlockLineWrapper = Support::CodeBlock::LineWrapper
|
|
29
|
+
CodeGroupHtmlBuilder = Support::CodeGroup::HtmlBuilder
|
|
30
|
+
|
|
31
|
+
def preprocess(content)
|
|
32
|
+
return content unless content.include?(":::code-group")
|
|
33
|
+
|
|
34
|
+
@code_block_ranges = find_code_block_ranges(content)
|
|
35
|
+
|
|
36
|
+
content.gsub(CODE_GROUP_PATTERN) do
|
|
37
|
+
match = ::Regexp.last_match
|
|
38
|
+
next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
|
|
39
|
+
|
|
40
|
+
process_code_group(match[1])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def process_code_group(inner_content)
|
|
47
|
+
blocks = extract_code_blocks(inner_content)
|
|
48
|
+
return "" if blocks.empty?
|
|
49
|
+
|
|
50
|
+
group_id = SecureRandom.hex(4)
|
|
51
|
+
html = CodeGroupHtmlBuilder.new(blocks, group_id).build
|
|
52
|
+
wrap_in_nomarkdown(html)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extract_code_blocks(content)
|
|
56
|
+
blocks = []
|
|
57
|
+
content.scan(CODE_BLOCK_PATTERN) do
|
|
58
|
+
blocks << build_block_data(::Regexp.last_match)
|
|
59
|
+
end
|
|
60
|
+
blocks
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_block_data(match)
|
|
64
|
+
lang = match[1] || ""
|
|
65
|
+
label = match[2]
|
|
66
|
+
options = match[3].strip
|
|
67
|
+
code = match[4]
|
|
68
|
+
|
|
69
|
+
code_with_newline = code.end_with?("\n") ? code : "#{code}\n"
|
|
70
|
+
markdown = "```#{lang}#{format_options(options)}\n#{code_with_newline}```"
|
|
71
|
+
extracted = CodeBlockFeatureExtractor.process_markdown(markdown)
|
|
72
|
+
rendered = render_and_enhance(extracted)
|
|
73
|
+
|
|
74
|
+
{ label: label, lang: lang, content: rendered, code_text: code.strip }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def format_options(options)
|
|
78
|
+
return "" if options.empty?
|
|
79
|
+
return options if options.start_with?(":")
|
|
80
|
+
|
|
81
|
+
" #{options}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_and_enhance(extracted)
|
|
85
|
+
html = render_markdown(extracted[:cleaned_markdown])
|
|
86
|
+
enhance_code_blocks(html, extracted[:blocks])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def render_markdown(markdown_content)
|
|
90
|
+
return "" if markdown_content.empty?
|
|
91
|
+
|
|
92
|
+
Kramdown::Document.new(
|
|
93
|
+
markdown_content,
|
|
94
|
+
input: "GFM",
|
|
95
|
+
hard_wrap: false,
|
|
96
|
+
syntax_highlighter: "rouge"
|
|
97
|
+
).to_html
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def enhance_code_blocks(html, blocks)
|
|
101
|
+
return html unless html.include?('<div class="highlight">')
|
|
102
|
+
|
|
103
|
+
block_index = 0
|
|
104
|
+
html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
|
|
105
|
+
block_data = blocks[block_index] || {}
|
|
106
|
+
block_index += 1
|
|
107
|
+
render_enhanced_code_block(Regexp.last_match, block_data)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_enhanced_code_block(match, block_data)
|
|
112
|
+
original_html = match[0]
|
|
113
|
+
inner_html = match[1]
|
|
114
|
+
code_text = extract_code_text(inner_html)
|
|
115
|
+
|
|
116
|
+
processed_html = process_html_if_needed(original_html, block_data)
|
|
117
|
+
Renderer.new.render_partial("_code_block", build_locals(processed_html, code_text, block_data))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def process_html_if_needed(original_html, block_data)
|
|
121
|
+
return original_html unless needs_line_wrapping?(block_data)
|
|
122
|
+
|
|
123
|
+
CodeBlockLineWrapper.wrap_code_block(original_html, wrapper_data(block_data))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def needs_line_wrapping?(block_data)
|
|
127
|
+
%i[highlights diff_lines focus_lines error_lines warning_lines].any? do |key|
|
|
128
|
+
block_data[key]&.any?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def wrapper_data(block_data)
|
|
133
|
+
{
|
|
134
|
+
highlights: block_data[:highlights] || [],
|
|
135
|
+
diff_lines: block_data[:diff_lines] || {},
|
|
136
|
+
focus_lines: block_data[:focus_lines] || {},
|
|
137
|
+
error_lines: block_data[:error_lines] || {},
|
|
138
|
+
warning_lines: block_data[:warning_lines] || {},
|
|
139
|
+
start_line: extract_start_line(block_data[:option])
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def build_locals(processed_html, code_text, block_data)
|
|
144
|
+
base_locals(processed_html, code_text, block_data).merge(feature_locals(block_data)).merge(title_locals)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def base_locals(processed_html, code_text, block_data)
|
|
148
|
+
show_ln = line_numbers_enabled?(block_data[:option])
|
|
149
|
+
start = extract_start_line(block_data[:option])
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
code_block_html: processed_html, code_text: escape_html_attribute(code_text),
|
|
153
|
+
copy_icon: Icons.render("copy", "regular") || "", show_line_numbers: show_ln,
|
|
154
|
+
line_numbers: show_ln ? generate_line_numbers(code_text, start) : [], start_line: start
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def feature_locals(block_data)
|
|
159
|
+
{
|
|
160
|
+
highlights: block_data[:highlights] || [], diff_lines: block_data[:diff_lines] || {},
|
|
161
|
+
focus_lines: block_data[:focus_lines] || {}, error_lines: block_data[:error_lines] || {},
|
|
162
|
+
warning_lines: block_data[:warning_lines] || {}
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def title_locals
|
|
167
|
+
{ title: nil, icon: nil, icon_source: nil }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def line_numbers_enabled?(option)
|
|
171
|
+
return false if option == ":no-line-numbers"
|
|
172
|
+
return true if option&.start_with?(":line-numbers")
|
|
173
|
+
|
|
174
|
+
false
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def extract_start_line(option)
|
|
178
|
+
return 1 unless option&.include?("=")
|
|
179
|
+
|
|
180
|
+
option.split("=").last.to_i
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def generate_line_numbers(code_text, start_line)
|
|
184
|
+
count = [code_text.lines.count, 1].max
|
|
185
|
+
(start_line...(start_line + count)).to_a
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def extract_code_text(html)
|
|
189
|
+
CGI.unescapeHTML(html.gsub(/<[^>]+>/, "")).strip
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def wrap_in_nomarkdown(html)
|
|
193
|
+
"{::nomarkdown}\n#{html}\n{:/nomarkdown}"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../base_processor"
|
|
4
|
+
require_relative "../support/markdown_code_block_helper"
|
|
4
5
|
|
|
5
6
|
module Docyard
|
|
6
7
|
module Components
|
|
7
8
|
module Processors
|
|
8
9
|
class CodeSnippetImportPreprocessor < BaseProcessor
|
|
10
|
+
include Support::MarkdownCodeBlockHelper
|
|
11
|
+
|
|
9
12
|
EXTENSION_MAP = {
|
|
10
13
|
"rb" => "ruby",
|
|
11
14
|
"js" => "javascript",
|
|
@@ -25,7 +28,9 @@ module Docyard
|
|
|
25
28
|
|
|
26
29
|
def preprocess(content)
|
|
27
30
|
@docs_root = context[:docs_root] || "docs"
|
|
28
|
-
content
|
|
31
|
+
process_outside_code_blocks(content) do |segment|
|
|
32
|
+
segment.gsub(IMPORT_PATTERN) { |_| process_import(Regexp.last_match) }
|
|
33
|
+
end
|
|
29
34
|
end
|
|
30
35
|
|
|
31
36
|
private
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base_processor"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Components
|
|
7
|
+
module Processors
|
|
8
|
+
class CustomAnchorProcessor < BaseProcessor
|
|
9
|
+
CUSTOM_ID_PATTERN = /\s*\{#([\w-]+)\}\s*$/
|
|
10
|
+
|
|
11
|
+
self.priority = 25
|
|
12
|
+
|
|
13
|
+
def postprocess(html)
|
|
14
|
+
process_custom_anchors(html)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def process_custom_anchors(html)
|
|
20
|
+
html.gsub(%r{<(h[1-6])(\s+id="[^"]*")?>(.+?)</\1>}m) do
|
|
21
|
+
tag = Regexp.last_match(1)
|
|
22
|
+
existing_attr = Regexp.last_match(2) || ""
|
|
23
|
+
content = Regexp.last_match(3)
|
|
24
|
+
|
|
25
|
+
if content.match?(CUSTOM_ID_PATTERN)
|
|
26
|
+
process_heading_with_custom_id(tag, content)
|
|
27
|
+
else
|
|
28
|
+
"<#{tag}#{existing_attr}>#{content}</#{tag}>"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def process_heading_with_custom_id(tag, content)
|
|
34
|
+
custom_id = content.match(CUSTOM_ID_PATTERN)[1]
|
|
35
|
+
clean_content = content.sub(CUSTOM_ID_PATTERN, "")
|
|
36
|
+
|
|
37
|
+
"<#{tag} id=\"#{custom_id}\">#{clean_content}</#{tag}>"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base_processor"
|
|
4
|
+
require_relative "../support/markdown_code_block_helper"
|
|
5
|
+
require_relative "../../rendering/icons"
|
|
6
|
+
|
|
7
|
+
module Docyard
|
|
8
|
+
module Components
|
|
9
|
+
module Processors
|
|
10
|
+
class FileTreeProcessor < BaseProcessor
|
|
11
|
+
include Support::MarkdownCodeBlockHelper
|
|
12
|
+
|
|
13
|
+
FILETREE_PATTERN = /```filetree\n(.*?)```/m
|
|
14
|
+
|
|
15
|
+
self.priority = 8
|
|
16
|
+
|
|
17
|
+
def preprocess(content)
|
|
18
|
+
@code_block_ranges = find_code_block_ranges(content, exclude_language: "filetree")
|
|
19
|
+
|
|
20
|
+
content.gsub(FILETREE_PATTERN) do
|
|
21
|
+
match = Regexp.last_match
|
|
22
|
+
next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
|
|
23
|
+
|
|
24
|
+
build_file_tree(match[1])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def build_file_tree(content)
|
|
31
|
+
lines = content.lines.map(&:chomp).reject(&:empty?)
|
|
32
|
+
|
|
33
|
+
items = parse_tree_structure(lines)
|
|
34
|
+
html = render_tree(items)
|
|
35
|
+
|
|
36
|
+
"\n\n<div class=\"docyard-filetree\" markdown=\"0\">\n#{html}</div>\n\n"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_tree_structure(lines)
|
|
40
|
+
root_items = []
|
|
41
|
+
stack = [{ indent: -1, children: root_items }]
|
|
42
|
+
|
|
43
|
+
lines.each { |line| process_line(line, stack) }
|
|
44
|
+
|
|
45
|
+
root_items
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def process_line(line, stack)
|
|
49
|
+
indent = line[/\A */].length
|
|
50
|
+
name = line.strip
|
|
51
|
+
return if name.empty?
|
|
52
|
+
|
|
53
|
+
item = build_item(name, indent)
|
|
54
|
+
unwind_stack(stack, indent)
|
|
55
|
+
stack.last[:children] << item
|
|
56
|
+
|
|
57
|
+
add_folder_to_stack(item, indent, stack) if item[:type] == :folder
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_item(name, indent)
|
|
61
|
+
item = parse_item(name)
|
|
62
|
+
item[:indent] = indent
|
|
63
|
+
item
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def unwind_stack(stack, indent)
|
|
67
|
+
stack.pop while stack.length > 1 && stack.last[:indent] >= indent
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def add_folder_to_stack(item, indent, stack)
|
|
71
|
+
item[:children] = []
|
|
72
|
+
stack.push({ indent: indent, children: item[:children] })
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_item(name)
|
|
76
|
+
highlighted = name.end_with?(" *")
|
|
77
|
+
name = name.chomp(" *") if highlighted
|
|
78
|
+
|
|
79
|
+
name, comment = extract_comment(name)
|
|
80
|
+
type = name.end_with?("/") ? :folder : :file
|
|
81
|
+
name = name.chomp("/") if type == :folder
|
|
82
|
+
|
|
83
|
+
{ name: name, type: type, highlighted: highlighted, comment: comment }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def extract_comment(name)
|
|
87
|
+
return [name, nil] unless name.include?(" # ")
|
|
88
|
+
|
|
89
|
+
name.split(" # ", 2)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_tree(items, depth = 0)
|
|
93
|
+
return "" if items.empty?
|
|
94
|
+
|
|
95
|
+
html = "<ul class=\"docyard-filetree__list\">\n"
|
|
96
|
+
items.each { |item| html += render_item(item, depth) }
|
|
97
|
+
html += "</ul>\n"
|
|
98
|
+
html
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_item(item, depth)
|
|
102
|
+
classes = item_classes(item)
|
|
103
|
+
|
|
104
|
+
html = "<li class=\"#{classes}\">\n"
|
|
105
|
+
html += render_entry(item)
|
|
106
|
+
html += render_tree(item[:children], depth + 1) if render_children?(item)
|
|
107
|
+
html += "</li>\n"
|
|
108
|
+
html
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def item_classes(item)
|
|
112
|
+
classes = ["docyard-filetree__item", "docyard-filetree__item--#{item[:type]}"]
|
|
113
|
+
classes << "docyard-filetree__item--highlighted" if item[:highlighted]
|
|
114
|
+
classes.join(" ")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render_entry(item)
|
|
118
|
+
html = "<span class=\"docyard-filetree__entry\">"
|
|
119
|
+
html += icon_for(item[:type])
|
|
120
|
+
html += "<span class=\"docyard-filetree__name\">#{escape_html(item[:name])}</span>"
|
|
121
|
+
html += render_comment(item[:comment])
|
|
122
|
+
html += "</span>"
|
|
123
|
+
html
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def render_comment(comment)
|
|
127
|
+
return "" unless comment
|
|
128
|
+
|
|
129
|
+
"<span class=\"docyard-filetree__comment\">#{escape_html(comment)}</span>"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def render_children?(item)
|
|
133
|
+
item[:type] == :folder && item[:children] && !item[:children].empty?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def icon_for(type)
|
|
137
|
+
icon_name = type == :folder ? "folder-open" : "file-text"
|
|
138
|
+
Icons.render(icon_name)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def escape_html(text)
|
|
142
|
+
text.to_s
|
|
143
|
+
.gsub("&", "&")
|
|
144
|
+
.gsub("<", "<")
|
|
145
|
+
.gsub(">", ">")
|
|
146
|
+
.gsub('"', """)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base_processor"
|
|
4
|
+
require_relative "../support/markdown_code_block_helper"
|
|
5
|
+
|
|
6
|
+
module Docyard
|
|
7
|
+
module Components
|
|
8
|
+
module Processors
|
|
9
|
+
class ImageCaptionProcessor < BaseProcessor
|
|
10
|
+
include Support::MarkdownCodeBlockHelper
|
|
11
|
+
|
|
12
|
+
IMAGE_ATTRS_PATTERN = /!\[([^\]]*)\]\(([^)]+)\)\{([^}]+)\}/
|
|
13
|
+
|
|
14
|
+
self.priority = 5
|
|
15
|
+
|
|
16
|
+
def preprocess(content)
|
|
17
|
+
process_outside_code_blocks(content) do |segment|
|
|
18
|
+
process_images_with_attrs(segment)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def process_images_with_attrs(content)
|
|
25
|
+
content.gsub(IMAGE_ATTRS_PATTERN) do
|
|
26
|
+
alt = Regexp.last_match(1)
|
|
27
|
+
src = Regexp.last_match(2)
|
|
28
|
+
attrs_string = Regexp.last_match(3)
|
|
29
|
+
|
|
30
|
+
attrs = parse_attributes(attrs_string)
|
|
31
|
+
build_image_html(alt, src, attrs)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_attributes(attrs_string)
|
|
36
|
+
attrs = {}
|
|
37
|
+
|
|
38
|
+
attrs_string.scan(/(\w+)="([^"]*)"/) do |key, value|
|
|
39
|
+
attrs[key] = value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attrs[:nozoom] = true if attrs_string.include?("nozoom")
|
|
43
|
+
|
|
44
|
+
attrs
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_image_html(alt, src, attrs)
|
|
48
|
+
if attrs["caption"]
|
|
49
|
+
build_figure(alt, src, attrs)
|
|
50
|
+
else
|
|
51
|
+
build_img(alt, src, attrs)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_figure(alt, src, attrs)
|
|
56
|
+
"\n\n" \
|
|
57
|
+
"<figure class=\"docyard-figure\" markdown=\"0\">\n" \
|
|
58
|
+
"#{build_img_tag(alt, src, attrs)}\n" \
|
|
59
|
+
"<figcaption>#{escape_html(attrs['caption'])}</figcaption>\n" \
|
|
60
|
+
"</figure>" \
|
|
61
|
+
"\n\n"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_img(alt, src, attrs)
|
|
65
|
+
"\n\n#{build_img_tag(alt, src, attrs)}\n\n"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_img_tag(alt, src, attrs)
|
|
69
|
+
parts = base_img_attrs(alt, src)
|
|
70
|
+
parts.concat(dimension_attrs(attrs))
|
|
71
|
+
parts << "data-no-zoom" if attrs[:nozoom]
|
|
72
|
+
"<img #{parts.join(' ')}>"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def base_img_attrs(alt, src)
|
|
76
|
+
["src=\"#{escape_html(src)}\"", "alt=\"#{escape_html(alt)}\""]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def dimension_attrs(attrs)
|
|
80
|
+
result = []
|
|
81
|
+
result << "width=\"#{escape_html(attrs['width'])}\"" if attrs["width"]
|
|
82
|
+
result << "height=\"#{escape_html(attrs['height'])}\"" if attrs["height"]
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def escape_html(text)
|
|
87
|
+
text.to_s
|
|
88
|
+
.gsub("&", "&")
|
|
89
|
+
.gsub("<", "<")
|
|
90
|
+
.gsub(">", ">")
|
|
91
|
+
.gsub('"', """)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base_processor"
|
|
4
|
+
require_relative "../support/markdown_code_block_helper"
|
|
5
|
+
|
|
6
|
+
module Docyard
|
|
7
|
+
module Components
|
|
8
|
+
module Processors
|
|
9
|
+
class IncludeProcessor < BaseProcessor
|
|
10
|
+
include Support::MarkdownCodeBlockHelper
|
|
11
|
+
|
|
12
|
+
INCLUDE_PATTERN = /<!--\s*@include:\s*([^\s]+)\s*-->/
|
|
13
|
+
|
|
14
|
+
self.priority = 0
|
|
15
|
+
|
|
16
|
+
def preprocess(content)
|
|
17
|
+
@current_file = context[:current_file]
|
|
18
|
+
@docs_root = context[:docs_root] || "docs"
|
|
19
|
+
@included_files = Set.new
|
|
20
|
+
|
|
21
|
+
process_outside_code_blocks(content) do |segment|
|
|
22
|
+
process_includes(segment)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def process_includes(content)
|
|
29
|
+
content.gsub(INCLUDE_PATTERN) { |_| process_include(Regexp.last_match) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def process_include(match)
|
|
33
|
+
filepath = match[1]
|
|
34
|
+
full_path = resolve_path(filepath)
|
|
35
|
+
|
|
36
|
+
error = validate_include(filepath, full_path)
|
|
37
|
+
return error if error
|
|
38
|
+
|
|
39
|
+
@included_files.add(full_path)
|
|
40
|
+
file_content = File.read(full_path)
|
|
41
|
+
|
|
42
|
+
process_includes(file_content.strip)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_include(filepath, full_path)
|
|
46
|
+
return include_error(filepath, "File not found") unless full_path && File.exist?(full_path)
|
|
47
|
+
return include_error(filepath, "Circular include detected") if @included_files.include?(full_path)
|
|
48
|
+
return include_error(filepath, "Use code snippets for non-markdown files") unless markdown_file?(filepath)
|
|
49
|
+
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def resolve_path(filepath)
|
|
54
|
+
if filepath.start_with?("./", "../")
|
|
55
|
+
resolve_relative_path(filepath)
|
|
56
|
+
else
|
|
57
|
+
resolve_docs_path(filepath)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resolve_relative_path(filepath)
|
|
62
|
+
return nil unless @current_file
|
|
63
|
+
|
|
64
|
+
base_dir = File.dirname(@current_file)
|
|
65
|
+
full_path = File.expand_path(filepath, base_dir)
|
|
66
|
+
|
|
67
|
+
full_path if File.exist?(full_path)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resolve_docs_path(filepath)
|
|
71
|
+
full_path = File.join(@docs_root, filepath)
|
|
72
|
+
full_path if File.exist?(full_path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def markdown_file?(filepath)
|
|
76
|
+
ext = File.extname(filepath).downcase
|
|
77
|
+
%w[.md .markdown .mdx].include?(ext)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def include_error(filepath, message)
|
|
81
|
+
"> [!WARNING]\n> Include error: #{filepath} - #{message}\n"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../rendering/renderer"
|
|
4
|
+
require_relative "../base_processor"
|
|
5
|
+
require_relative "../support/markdown_code_block_helper"
|
|
6
|
+
require "kramdown"
|
|
7
|
+
require "kramdown-parser-gfm"
|
|
8
|
+
|
|
9
|
+
module Docyard
|
|
10
|
+
module Components
|
|
11
|
+
module Processors
|
|
12
|
+
class StepsProcessor < BaseProcessor
|
|
13
|
+
include Support::MarkdownCodeBlockHelper
|
|
14
|
+
|
|
15
|
+
self.priority = 10
|
|
16
|
+
|
|
17
|
+
STEPS_PATTERN = /^:::steps\s*\n(.*?)^:::\s*$/m
|
|
18
|
+
STEP_HEADING_PATTERN = /^###\s+(.+)$/
|
|
19
|
+
|
|
20
|
+
def preprocess(markdown)
|
|
21
|
+
@code_block_ranges = find_code_block_ranges(markdown)
|
|
22
|
+
|
|
23
|
+
markdown.gsub(STEPS_PATTERN) do
|
|
24
|
+
match = Regexp.last_match
|
|
25
|
+
next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
|
|
26
|
+
|
|
27
|
+
content = match[1]
|
|
28
|
+
steps = parse_steps(content)
|
|
29
|
+
|
|
30
|
+
wrap_in_nomarkdown(render_steps_html(steps))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def parse_steps(content)
|
|
37
|
+
steps = []
|
|
38
|
+
current_step = nil
|
|
39
|
+
|
|
40
|
+
content.lines.each do |line|
|
|
41
|
+
if line.match(STEP_HEADING_PATTERN)
|
|
42
|
+
steps << current_step if current_step
|
|
43
|
+
current_step = { title: Regexp.last_match(1).strip, content: "" }
|
|
44
|
+
elsif current_step
|
|
45
|
+
current_step[:content] += line
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
steps << current_step if current_step
|
|
50
|
+
steps
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_steps_html(steps)
|
|
54
|
+
renderer = Renderer.new
|
|
55
|
+
|
|
56
|
+
steps_html = steps.map.with_index(1) do |step, index|
|
|
57
|
+
content_html = render_markdown_content(step[:content].strip)
|
|
58
|
+
|
|
59
|
+
renderer.render_partial(
|
|
60
|
+
"_step", {
|
|
61
|
+
number: index,
|
|
62
|
+
title: step[:title],
|
|
63
|
+
content_html: content_html,
|
|
64
|
+
is_last: index == steps.length
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
end.join("\n")
|
|
68
|
+
|
|
69
|
+
"<div class=\"docyard-steps\">\n#{steps_html}\n</div>"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_markdown_content(content_markdown)
|
|
73
|
+
return "" if content_markdown.empty?
|
|
74
|
+
|
|
75
|
+
Kramdown::Document.new(
|
|
76
|
+
content_markdown,
|
|
77
|
+
input: "GFM",
|
|
78
|
+
hard_wrap: false,
|
|
79
|
+
syntax_highlighter: "rouge"
|
|
80
|
+
).to_html
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def wrap_in_nomarkdown(html)
|
|
84
|
+
"{::nomarkdown}\n#{html}\n{:/nomarkdown}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|