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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../rendering/renderer"
|
|
4
4
|
require_relative "../base_processor"
|
|
5
|
+
require_relative "../support/markdown_code_block_helper"
|
|
5
6
|
require_relative "../support/tabs/parser"
|
|
6
7
|
require "securerandom"
|
|
7
8
|
|
|
@@ -9,6 +10,8 @@ module Docyard
|
|
|
9
10
|
module Components
|
|
10
11
|
module Processors
|
|
11
12
|
class TabsProcessor < BaseProcessor
|
|
13
|
+
include Support::MarkdownCodeBlockHelper
|
|
14
|
+
|
|
12
15
|
self.priority = 15
|
|
13
16
|
|
|
14
17
|
TabsParser = Support::Tabs::Parser
|
|
@@ -16,8 +19,13 @@ module Docyard
|
|
|
16
19
|
def preprocess(content)
|
|
17
20
|
return content unless content.include?(":::tabs")
|
|
18
21
|
|
|
22
|
+
@code_block_ranges = find_code_block_ranges(content)
|
|
23
|
+
|
|
19
24
|
content.gsub(/^:::[ \t]*tabs[ \t]*\n(.*?)^:::[ \t]*$/m) do
|
|
20
|
-
|
|
25
|
+
match = Regexp.last_match
|
|
26
|
+
next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
|
|
27
|
+
|
|
28
|
+
process_tabs_block(match[1])
|
|
21
29
|
end
|
|
22
30
|
end
|
|
23
31
|
|
|
@@ -0,0 +1,57 @@
|
|
|
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 TooltipProcessor < BaseProcessor
|
|
10
|
+
include Support::MarkdownCodeBlockHelper
|
|
11
|
+
|
|
12
|
+
TOOLTIP_PATTERN = /:tooltip\[([^\]]+)\]\{([^}]+)\}/
|
|
13
|
+
self.priority = 6
|
|
14
|
+
|
|
15
|
+
def preprocess(content)
|
|
16
|
+
process_outside_code_blocks(content) do |segment|
|
|
17
|
+
segment.gsub(TOOLTIP_PATTERN) do |_match|
|
|
18
|
+
term = ::Regexp.last_match(1)
|
|
19
|
+
attributes = parse_attributes(::Regexp.last_match(2))
|
|
20
|
+
build_tooltip_tag(term, attributes)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def parse_attributes(attr_string)
|
|
28
|
+
attributes = {}
|
|
29
|
+
attr_string.scan(/(\w+)="([^"]*)"/) do |key, value|
|
|
30
|
+
attributes[key.to_sym] = value
|
|
31
|
+
end
|
|
32
|
+
attributes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_tooltip_tag(term, attributes)
|
|
36
|
+
description = escape_html(attributes[:description] || "")
|
|
37
|
+
link = attributes[:link]
|
|
38
|
+
link_text = attributes[:link_text] || "Learn more"
|
|
39
|
+
|
|
40
|
+
data_attrs = %(data-description="#{description}")
|
|
41
|
+
data_attrs += %( data-link="#{escape_html(link)}") if link
|
|
42
|
+
data_attrs += %( data-link-text="#{escape_html(link_text)}") if link
|
|
43
|
+
|
|
44
|
+
%(<span class="docyard-tooltip" #{data_attrs}>#{term}</span>)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def escape_html(text)
|
|
48
|
+
text.to_s
|
|
49
|
+
.gsub("&", "&")
|
|
50
|
+
.gsub("<", "<")
|
|
51
|
+
.gsub(">", ">")
|
|
52
|
+
.gsub('"', """)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
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 VideoEmbedProcessor < BaseProcessor
|
|
10
|
+
include Support::MarkdownCodeBlockHelper
|
|
11
|
+
|
|
12
|
+
YOUTUBE_PATTERN = /::youtube\[([^\]]+)\](?:\{([^}]*)\})?/
|
|
13
|
+
VIMEO_PATTERN = /::vimeo\[([^\]]+)\](?:\{([^}]*)\})?/
|
|
14
|
+
VIDEO_PATTERN = /::video\[([^\]]+)\](?:\{([^}]*)\})?/
|
|
15
|
+
|
|
16
|
+
self.priority = 5
|
|
17
|
+
|
|
18
|
+
def preprocess(content)
|
|
19
|
+
process_outside_code_blocks(content) do |segment|
|
|
20
|
+
process_video_embeds(segment)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def process_video_embeds(content)
|
|
27
|
+
result = content.gsub(YOUTUBE_PATTERN) do
|
|
28
|
+
video_id = Regexp.last_match(1)
|
|
29
|
+
attrs_string = Regexp.last_match(2)
|
|
30
|
+
build_youtube_embed(video_id, parse_attributes(attrs_string))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result = result.gsub(VIMEO_PATTERN) do
|
|
34
|
+
video_id = Regexp.last_match(1)
|
|
35
|
+
attrs_string = Regexp.last_match(2)
|
|
36
|
+
build_vimeo_embed(video_id, parse_attributes(attrs_string))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
result.gsub(VIDEO_PATTERN) do
|
|
40
|
+
src = Regexp.last_match(1)
|
|
41
|
+
attrs_string = Regexp.last_match(2)
|
|
42
|
+
build_native_video(src, parse_attributes(attrs_string))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def parse_attributes(attrs_string)
|
|
47
|
+
return {} if attrs_string.nil? || attrs_string.empty?
|
|
48
|
+
|
|
49
|
+
attrs = {}
|
|
50
|
+
|
|
51
|
+
attrs_string.scan(/(\w+)="([^"]*)"/) do |key, value|
|
|
52
|
+
attrs[key] = value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
%w[autoplay loop muted nofullscreen controls playsinline].each do |flag|
|
|
56
|
+
attrs[flag] = true if attrs_string.match?(/\b#{flag}\b/) && !attrs.key?(flag)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attrs
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_youtube_embed(video_id, attrs)
|
|
63
|
+
params = youtube_params(attrs)
|
|
64
|
+
url = "https://www.youtube-nocookie.com/embed/#{escape_attr(video_id)}"
|
|
65
|
+
url += "?#{params.join('&')}" unless params.empty?
|
|
66
|
+
|
|
67
|
+
build_iframe(url, attrs, "youtube")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_vimeo_embed(video_id, attrs)
|
|
71
|
+
params = vimeo_params(attrs)
|
|
72
|
+
url = "https://player.vimeo.com/video/#{escape_attr(video_id)}"
|
|
73
|
+
url += "?#{params.join('&')}" unless params.empty?
|
|
74
|
+
|
|
75
|
+
build_iframe(url, attrs, "vimeo")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_native_video(src, attrs)
|
|
79
|
+
wrapper_style = build_wrapper_style(attrs)
|
|
80
|
+
video_attrs = build_video_attrs(src, attrs)
|
|
81
|
+
|
|
82
|
+
"\n\n" \
|
|
83
|
+
"<div class=\"docyard-video docyard-video--native\"#{wrapper_style} markdown=\"0\">\n " \
|
|
84
|
+
"<video #{video_attrs.join(' ')}></video>\n" \
|
|
85
|
+
"</div>" \
|
|
86
|
+
"\n\n"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_video_attrs(src, attrs)
|
|
90
|
+
video_attrs = ["src=\"#{escape_attr(src)}\""]
|
|
91
|
+
video_attrs.concat(video_optional_attrs(attrs))
|
|
92
|
+
video_attrs.concat(video_boolean_attrs(attrs))
|
|
93
|
+
video_attrs
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def video_optional_attrs(attrs)
|
|
97
|
+
result = []
|
|
98
|
+
result << "poster=\"#{escape_attr(attrs['poster'])}\"" if attrs["poster"]
|
|
99
|
+
result << "preload=\"#{escape_attr(attrs['preload'])}\"" if attrs["preload"]
|
|
100
|
+
result
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def video_boolean_attrs(attrs)
|
|
104
|
+
result = []
|
|
105
|
+
result << "controls" unless controls_disabled?(attrs)
|
|
106
|
+
result << "autoplay" if attrs["autoplay"]
|
|
107
|
+
result << "muted" if attrs["muted"]
|
|
108
|
+
result << "loop" if attrs["loop"]
|
|
109
|
+
result << "playsinline" if attrs["playsinline"]
|
|
110
|
+
result
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def controls_disabled?(attrs)
|
|
114
|
+
["false", false].include?(attrs["controls"])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def youtube_params(attrs)
|
|
118
|
+
params = []
|
|
119
|
+
params << "autoplay=1" if attrs["autoplay"]
|
|
120
|
+
params << "loop=1" if attrs["loop"]
|
|
121
|
+
params << "mute=1" if attrs["muted"]
|
|
122
|
+
params << "controls=0" if ["false", false].include?(attrs["controls"])
|
|
123
|
+
params << "start=#{attrs['start']}" if attrs["start"]
|
|
124
|
+
params << "rel=0"
|
|
125
|
+
params
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def vimeo_params(attrs)
|
|
129
|
+
params = []
|
|
130
|
+
params << "autoplay=1" if attrs["autoplay"]
|
|
131
|
+
params << "loop=1" if attrs["loop"]
|
|
132
|
+
params << "muted=1" if attrs["muted"]
|
|
133
|
+
params << "controls=0" if ["false", false].include?(attrs["controls"])
|
|
134
|
+
params << "dnt=1"
|
|
135
|
+
params
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_iframe(url, attrs, provider)
|
|
139
|
+
wrapper_style = build_wrapper_style(attrs)
|
|
140
|
+
iframe_attrs = build_iframe_attrs(url, attrs, provider)
|
|
141
|
+
|
|
142
|
+
"\n\n" \
|
|
143
|
+
"<div class=\"docyard-video docyard-video--#{provider}\"#{wrapper_style} markdown=\"0\">\n " \
|
|
144
|
+
"<iframe #{iframe_attrs.join(' ')}></iframe>\n" \
|
|
145
|
+
"</div>" \
|
|
146
|
+
"\n\n"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def build_wrapper_style(attrs)
|
|
150
|
+
return "" unless attrs["width"] || attrs["height"]
|
|
151
|
+
|
|
152
|
+
styles = []
|
|
153
|
+
styles << "max-width: #{escape_attr(attrs['width'])}px" if attrs["width"]
|
|
154
|
+
styles << "height: #{escape_attr(attrs['height'])}px" if attrs["height"]
|
|
155
|
+
" style=\"#{styles.join('; ')}\""
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def build_iframe_attrs(url, attrs, provider)
|
|
159
|
+
iframe_attrs = [
|
|
160
|
+
"src=\"#{url}\"",
|
|
161
|
+
"title=\"#{escape_attr(attrs['title'] || default_title(provider))}\"",
|
|
162
|
+
"frameborder=\"0\""
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
iframe_attrs << "allow=\"#{build_allow_attr(attrs)}\""
|
|
166
|
+
iframe_attrs << "allowfullscreen" unless attrs["nofullscreen"]
|
|
167
|
+
|
|
168
|
+
iframe_attrs
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def build_allow_attr(attrs)
|
|
172
|
+
permissions = %w[encrypted-media picture-in-picture web-share]
|
|
173
|
+
permissions.unshift("autoplay") if attrs["autoplay"]
|
|
174
|
+
permissions << "fullscreen" unless attrs["nofullscreen"]
|
|
175
|
+
permissions.join("; ")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def default_title(provider)
|
|
179
|
+
case provider
|
|
180
|
+
when "youtube" then "YouTube video player"
|
|
181
|
+
when "vimeo" then "Vimeo video player"
|
|
182
|
+
else "Video player"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def escape_attr(text)
|
|
187
|
+
text.to_s
|
|
188
|
+
.gsub("&", "&")
|
|
189
|
+
.gsub("<", "<")
|
|
190
|
+
.gsub(">", ">")
|
|
191
|
+
.gsub('"', """)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../code_block/icon_detector"
|
|
4
|
+
require_relative "../../../rendering/icons"
|
|
5
|
+
|
|
6
|
+
module Docyard
|
|
7
|
+
module Components
|
|
8
|
+
module Support
|
|
9
|
+
module CodeGroup
|
|
10
|
+
class HtmlBuilder
|
|
11
|
+
def initialize(blocks, group_id)
|
|
12
|
+
@blocks = blocks
|
|
13
|
+
@group_id = group_id
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build
|
|
17
|
+
<<~HTML
|
|
18
|
+
<div class="docyard-code-group" data-code-group="#{@group_id}">
|
|
19
|
+
<div class="docyard-code-group__tabs-wrapper">
|
|
20
|
+
<div class="docyard-code-group__tabs-scroll-container">
|
|
21
|
+
<div role="tablist" aria-label="Code examples" class="docyard-code-group__tabs">
|
|
22
|
+
#{build_tabs}
|
|
23
|
+
<div class="docyard-code-group__indicator" aria-hidden="true"></div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
#{build_copy_button}
|
|
27
|
+
</div>
|
|
28
|
+
<div class="docyard-code-group__panels">
|
|
29
|
+
#{build_panels}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
HTML
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_tabs
|
|
38
|
+
@blocks.each_with_index.map do |block, index|
|
|
39
|
+
build_tab(block, index)
|
|
40
|
+
end.join("\n")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_tab(block, index)
|
|
44
|
+
selected = index.zero? ? "true" : "false"
|
|
45
|
+
tabindex = index.zero? ? "0" : "-1"
|
|
46
|
+
icon_html = render_icon(block[:lang])
|
|
47
|
+
<<~HTML.strip
|
|
48
|
+
<button
|
|
49
|
+
role="tab"
|
|
50
|
+
aria-selected="#{selected}"
|
|
51
|
+
aria-controls="cg-panel-#{@group_id}-#{index}"
|
|
52
|
+
id="cg-tab-#{@group_id}-#{index}"
|
|
53
|
+
class="docyard-code-group__tab"
|
|
54
|
+
tabindex="#{tabindex}"
|
|
55
|
+
data-label="#{escape_html(block[:label])}"
|
|
56
|
+
>#{icon_html}#{escape_html(block[:label])}</button>
|
|
57
|
+
HTML
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def render_icon(lang)
|
|
61
|
+
return "" if lang.nil? || lang.empty?
|
|
62
|
+
|
|
63
|
+
icon, icon_source = CodeBlock::IconDetector.auto_detect_icon(lang)
|
|
64
|
+
return "" unless icon && icon_source == "file-extension"
|
|
65
|
+
|
|
66
|
+
Icons.render_file_extension(icon) || ""
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_copy_button
|
|
70
|
+
copy_icon = Icons.render("copy", "regular") || ""
|
|
71
|
+
<<~HTML.strip
|
|
72
|
+
<button class="docyard-code-group__copy" aria-label="Copy code to clipboard">
|
|
73
|
+
<span class="docyard-code-group__copy-icon">#{copy_icon}</span>
|
|
74
|
+
<span class="docyard-code-group__copy-text">Copy</span>
|
|
75
|
+
</button>
|
|
76
|
+
HTML
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_panels
|
|
80
|
+
@blocks.each_with_index.map do |block, index|
|
|
81
|
+
build_panel(block, index)
|
|
82
|
+
end.join("\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_panel(block, index)
|
|
86
|
+
hidden = index.zero? ? "false" : "true"
|
|
87
|
+
code_text = escape_html_attribute(block[:code_text] || "")
|
|
88
|
+
<<~HTML.strip
|
|
89
|
+
<div
|
|
90
|
+
role="tabpanel"
|
|
91
|
+
id="cg-panel-#{@group_id}-#{index}"
|
|
92
|
+
aria-labelledby="cg-tab-#{@group_id}-#{index}"
|
|
93
|
+
aria-hidden="#{hidden}"
|
|
94
|
+
class="docyard-code-group__panel"
|
|
95
|
+
tabindex="0"
|
|
96
|
+
data-code="#{code_text}"
|
|
97
|
+
>#{block[:content]}</div>
|
|
98
|
+
HTML
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def escape_html(text)
|
|
102
|
+
text.to_s
|
|
103
|
+
.gsub("&", "&")
|
|
104
|
+
.gsub("<", "<")
|
|
105
|
+
.gsub(">", ">")
|
|
106
|
+
.gsub('"', """)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def escape_html_attribute(text)
|
|
110
|
+
text.to_s
|
|
111
|
+
.gsub("&", "&")
|
|
112
|
+
.gsub("<", "<")
|
|
113
|
+
.gsub(">", ">")
|
|
114
|
+
.gsub('"', """)
|
|
115
|
+
.gsub("'", "'")
|
|
116
|
+
.gsub("\n", " ")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
module Support
|
|
6
|
+
module MarkdownCodeBlockHelper
|
|
7
|
+
FENCED_CODE_BLOCK_REGEX = /^(`{3,}|~{3,})[^\n]*\n.*?^\1\s*$/m
|
|
8
|
+
|
|
9
|
+
def process_outside_code_blocks(content)
|
|
10
|
+
segments = split_by_code_blocks(content)
|
|
11
|
+
|
|
12
|
+
segments.map do |segment|
|
|
13
|
+
segment[:type] == :code ? segment[:content] : yield(segment[:content])
|
|
14
|
+
end.join
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def find_code_block_ranges(content, exclude_language: nil)
|
|
18
|
+
ranges = []
|
|
19
|
+
content.scan(FENCED_CODE_BLOCK_REGEX) do
|
|
20
|
+
match = Regexp.last_match
|
|
21
|
+
next if exclude_language && match[0] =~ /\A[`~]{3,}#{exclude_language}\b/
|
|
22
|
+
|
|
23
|
+
ranges << (match.begin(0)...match.end(0))
|
|
24
|
+
end
|
|
25
|
+
ranges
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def inside_code_block?(position, ranges)
|
|
29
|
+
ranges.any? { |range| range.cover?(position) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def split_by_code_blocks(content)
|
|
35
|
+
segments = []
|
|
36
|
+
current_pos = 0
|
|
37
|
+
|
|
38
|
+
content.scan(FENCED_CODE_BLOCK_REGEX) do
|
|
39
|
+
match = Regexp.last_match
|
|
40
|
+
match_start = match.begin(0)
|
|
41
|
+
match_end = match.end(0)
|
|
42
|
+
|
|
43
|
+
segments << { type: :text, content: content[current_pos...match_start] } if match_start > current_pos
|
|
44
|
+
|
|
45
|
+
segments << { type: :code, content: content[match_start...match_end] }
|
|
46
|
+
current_pos = match_end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
segments << { type: :text, content: content[current_pos..] } if current_pos < content.length
|
|
50
|
+
|
|
51
|
+
segments.empty? ? [{ type: :text, content: content }] : segments
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "logo_detector"
|
|
4
|
+
|
|
3
5
|
module Docyard
|
|
4
6
|
class BrandingResolver
|
|
5
7
|
def initialize(config)
|
|
@@ -45,59 +47,28 @@ module Docyard
|
|
|
45
47
|
.merge(social_options)
|
|
46
48
|
.merge(navigation_options)
|
|
47
49
|
.merge(tabs_options)
|
|
50
|
+
.merge(announcement_options)
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
def site_options
|
|
51
54
|
{
|
|
52
55
|
site_title: config.title || Constants::DEFAULT_SITE_TITLE,
|
|
53
56
|
site_description: config.description || "",
|
|
54
|
-
favicon: config.branding.favicon || auto_detect_favicon
|
|
57
|
+
favicon: config.branding.favicon || LogoDetector.auto_detect_favicon
|
|
55
58
|
}
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
def logo_options
|
|
59
62
|
branding = config.branding
|
|
60
|
-
logo = branding.logo || auto_detect_logo
|
|
63
|
+
logo = branding.logo || LogoDetector.auto_detect_logo
|
|
61
64
|
has_custom_logo = !logo.nil?
|
|
62
65
|
{
|
|
63
66
|
logo: logo || Constants::DEFAULT_LOGO_PATH,
|
|
64
|
-
logo_dark: detect_dark_logo(logo) || Constants::DEFAULT_LOGO_DARK_PATH,
|
|
67
|
+
logo_dark: LogoDetector.detect_dark_logo(logo) || Constants::DEFAULT_LOGO_DARK_PATH,
|
|
65
68
|
has_custom_logo: has_custom_logo
|
|
66
69
|
}
|
|
67
70
|
end
|
|
68
71
|
|
|
69
|
-
def auto_detect_logo
|
|
70
|
-
detect_public_file("logo", %w[svg png])
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def auto_detect_favicon
|
|
74
|
-
detect_public_file("favicon", %w[ico svg png])
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def detect_public_file(name, extensions)
|
|
78
|
-
extensions.each do |ext|
|
|
79
|
-
path = File.join(Constants::PUBLIC_DIR, "#{name}.#{ext}")
|
|
80
|
-
return "#{name}.#{ext}" if File.exist?(path)
|
|
81
|
-
end
|
|
82
|
-
nil
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def detect_dark_logo(logo)
|
|
86
|
-
return nil unless logo
|
|
87
|
-
|
|
88
|
-
ext = File.extname(logo)
|
|
89
|
-
base = File.basename(logo, ext)
|
|
90
|
-
dark_filename = "#{base}-dark#{ext}"
|
|
91
|
-
|
|
92
|
-
if File.absolute_path?(logo)
|
|
93
|
-
dark_path = File.join(File.dirname(logo), dark_filename)
|
|
94
|
-
File.exist?(dark_path) ? dark_path : logo
|
|
95
|
-
else
|
|
96
|
-
dark_path = File.join("docs/public", dark_filename)
|
|
97
|
-
File.exist?(dark_path) ? dark_filename : logo
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
72
|
def search_options
|
|
102
73
|
{
|
|
103
74
|
search_enabled: config.search.enabled != false,
|
|
@@ -179,5 +150,29 @@ module Docyard
|
|
|
179
150
|
}
|
|
180
151
|
end
|
|
181
152
|
end
|
|
153
|
+
|
|
154
|
+
def announcement_options
|
|
155
|
+
announcement = config.announcement
|
|
156
|
+
return { announcement: nil } unless announcement
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
announcement: {
|
|
160
|
+
text: announcement.text,
|
|
161
|
+
link: announcement.link,
|
|
162
|
+
button: build_announcement_button(announcement),
|
|
163
|
+
dismissible: announcement.dismissible != false
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def build_announcement_button(announcement)
|
|
169
|
+
button = announcement.button
|
|
170
|
+
return nil unless button.is_a?(Hash) && button["text"]
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
text: button["text"],
|
|
174
|
+
link: button["link"] || announcement.link
|
|
175
|
+
}
|
|
176
|
+
end
|
|
182
177
|
end
|
|
183
178
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module LogoDetector
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def auto_detect_logo
|
|
8
|
+
detect_public_file("logo", %w[svg png])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def auto_detect_favicon
|
|
12
|
+
detect_public_file("favicon", %w[ico svg png])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def detect_public_file(name, extensions)
|
|
16
|
+
extensions.each do |ext|
|
|
17
|
+
path = File.join(Constants::PUBLIC_DIR, "#{name}.#{ext}")
|
|
18
|
+
return "#{name}.#{ext}" if File.exist?(path)
|
|
19
|
+
end
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def detect_dark_logo(logo)
|
|
24
|
+
return nil unless logo
|
|
25
|
+
|
|
26
|
+
ext = File.extname(logo)
|
|
27
|
+
base = File.basename(logo, ext)
|
|
28
|
+
dark_filename = "#{base}-dark#{ext}"
|
|
29
|
+
|
|
30
|
+
if File.absolute_path?(logo)
|
|
31
|
+
dark_path = File.join(File.dirname(logo), dark_filename)
|
|
32
|
+
File.exist?(dark_path) ? dark_path : logo
|
|
33
|
+
else
|
|
34
|
+
dark_path = File.join("docs/public", dark_filename)
|
|
35
|
+
File.exist?(dark_path) ? dark_filename : logo
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/docyard/config.rb
CHANGED
|
@@ -29,7 +29,8 @@ module Docyard
|
|
|
29
29
|
"navigation" => {
|
|
30
30
|
"cta" => [],
|
|
31
31
|
"breadcrumbs" => true
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"announcement" => nil
|
|
33
34
|
}.freeze
|
|
34
35
|
|
|
35
36
|
attr_reader :data, :file_path
|
|
@@ -81,6 +82,10 @@ module Docyard
|
|
|
81
82
|
@navigation ||= ConfigSection.new(data["navigation"])
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
def announcement
|
|
86
|
+
@announcement ||= data["announcement"] ? ConfigSection.new(data["announcement"]) : nil
|
|
87
|
+
end
|
|
88
|
+
|
|
84
89
|
private
|
|
85
90
|
|
|
86
91
|
def load_config_data
|
|
@@ -58,17 +58,29 @@ module Docyard
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def build_context(slug, base_path, options)
|
|
61
|
+
paths = resolve_paths(slug, base_path, options)
|
|
62
|
+
frontmatter = metadata_extractor.extract_frontmatter_metadata(paths[:file])
|
|
63
|
+
|
|
64
|
+
build_context_hash(slug, paths, options, frontmatter)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def resolve_paths(slug, base_path, options)
|
|
61
68
|
file_path = File.join(docs_path, base_path, "#{slug}.md")
|
|
62
69
|
url_path = Utils::PathResolver.to_url(File.join(base_path, slug))
|
|
63
|
-
frontmatter = metadata_extractor.extract_frontmatter_metadata(file_path)
|
|
64
70
|
final_path = options["link"] || options[:link] || url_path
|
|
65
71
|
|
|
72
|
+
{ file: file_path, final: final_path }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_context_hash(slug, paths, options, frontmatter)
|
|
66
76
|
{
|
|
67
77
|
slug: slug,
|
|
68
|
-
text: metadata_extractor.resolve_item_text(slug,
|
|
69
|
-
path:
|
|
78
|
+
text: metadata_extractor.resolve_item_text(slug, paths[:file], options, frontmatter[:text]),
|
|
79
|
+
path: paths[:final],
|
|
70
80
|
icon: metadata_extractor.resolve_item_icon(options, frontmatter[:icon]),
|
|
71
|
-
|
|
81
|
+
badge: frontmatter[:badge],
|
|
82
|
+
badge_type: frontmatter[:badge_type],
|
|
83
|
+
active: current_path == paths[:final],
|
|
72
84
|
type: :file,
|
|
73
85
|
section: false
|
|
74
86
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Docyard
|
|
4
4
|
module Sidebar
|
|
5
5
|
class Item
|
|
6
|
-
attr_reader :slug, :text, :icon, :link, :target, :collapsed, :items, :path, :active, :type, :has_index, :section
|
|
6
|
+
attr_reader :slug, :text, :icon, :link, :target, :collapsed, :items, :path, :active, :type, :has_index, :section,
|
|
7
|
+
:badge, :badge_type
|
|
7
8
|
|
|
8
9
|
DEFAULTS = {
|
|
9
10
|
target: "_self",
|
|
@@ -27,6 +28,8 @@ module Docyard
|
|
|
27
28
|
@text = options[:text]
|
|
28
29
|
@icon = options[:icon]
|
|
29
30
|
@link = options[:link]
|
|
31
|
+
@badge = options[:badge]
|
|
32
|
+
@badge_type = options[:badge_type]
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def assign_optional_attributes(options)
|
|
@@ -88,6 +91,8 @@ module Docyard
|
|
|
88
91
|
target: target,
|
|
89
92
|
has_index: has_index,
|
|
90
93
|
section: section,
|
|
94
|
+
badge: badge,
|
|
95
|
+
badge_type: badge_type,
|
|
91
96
|
children: children.map(&:to_h)
|
|
92
97
|
}
|
|
93
98
|
end
|