docyard 0.3.0 → 0.5.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 +32 -2
- data/README.md +80 -33
- data/lib/docyard/build/asset_bundler.rb +139 -0
- data/lib/docyard/build/file_copier.rb +105 -0
- data/lib/docyard/build/sitemap_generator.rb +57 -0
- data/lib/docyard/build/static_generator.rb +141 -0
- data/lib/docyard/builder.rb +104 -0
- data/lib/docyard/cli.rb +19 -0
- data/lib/docyard/components/heading_anchor_processor.rb +34 -0
- data/lib/docyard/components/table_of_contents_processor.rb +64 -0
- data/lib/docyard/components/table_wrapper_processor.rb +18 -0
- data/lib/docyard/config.rb +15 -2
- data/lib/docyard/icons/phosphor.rb +3 -1
- data/lib/docyard/initializer.rb +80 -14
- data/lib/docyard/markdown.rb +19 -0
- data/lib/docyard/prev_next_builder.rb +159 -0
- data/lib/docyard/preview_server.rb +72 -0
- data/lib/docyard/rack_application.rb +25 -3
- data/lib/docyard/renderer.rb +33 -8
- data/lib/docyard/sidebar/config_parser.rb +180 -0
- data/lib/docyard/sidebar/item.rb +58 -0
- data/lib/docyard/sidebar/renderer.rb +33 -6
- data/lib/docyard/sidebar_builder.rb +45 -1
- data/lib/docyard/templates/assets/css/components/callout.css +1 -1
- data/lib/docyard/templates/assets/css/components/code-block.css +2 -2
- data/lib/docyard/templates/assets/css/components/heading-anchor.css +77 -0
- data/lib/docyard/templates/assets/css/components/navigation.css +65 -7
- data/lib/docyard/templates/assets/css/components/prev-next.css +114 -0
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +269 -0
- data/lib/docyard/templates/assets/css/components/tabs.css +3 -2
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +8 -0
- data/lib/docyard/templates/assets/css/layout.css +58 -1
- data/lib/docyard/templates/assets/css/markdown.css +20 -11
- data/lib/docyard/templates/assets/css/variables.css +1 -0
- data/lib/docyard/templates/assets/js/components/heading-anchor.js +90 -0
- data/lib/docyard/templates/assets/js/components/navigation.js +225 -0
- data/lib/docyard/templates/assets/js/components/table-of-contents.js +301 -0
- data/lib/docyard/templates/assets/js/theme.js +2 -185
- data/lib/docyard/templates/config/docyard.yml.erb +32 -10
- data/lib/docyard/templates/layouts/default.html.erb +10 -2
- data/lib/docyard/templates/markdown/getting-started/installation.md.erb +46 -12
- data/lib/docyard/templates/markdown/guides/configuration.md.erb +202 -0
- data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +247 -0
- data/lib/docyard/templates/markdown/index.md.erb +55 -59
- data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -0
- data/lib/docyard/templates/partials/_nav_group.html.erb +10 -4
- data/lib/docyard/templates/partials/_nav_leaf.html.erb +9 -1
- data/lib/docyard/templates/partials/_prev_next.html.erb +23 -0
- data/lib/docyard/templates/partials/_table_of_contents.html.erb +45 -0
- data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +8 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +8 -0
- metadata +67 -10
- data/lib/docyard/templates/markdown/components/callouts.md.erb +0 -204
- data/lib/docyard/templates/markdown/components/icons.md.erb +0 -125
- data/lib/docyard/templates/markdown/components/tabs.md.erb +0 -686
- data/lib/docyard/templates/markdown/configuration.md.erb +0 -202
- data/lib/docyard/templates/markdown/core-concepts/file-structure.md.erb +0 -61
- data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +0 -90
- data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +0 -30
- data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +0 -56
- data/lib/docyard/templates/partials/_icons.html.erb +0 -11
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "rack"
|
|
5
5
|
require_relative "sidebar_builder"
|
|
6
|
+
require_relative "prev_next_builder"
|
|
6
7
|
require_relative "constants"
|
|
7
8
|
|
|
8
9
|
module Docyard
|
|
@@ -12,7 +13,7 @@ module Docyard
|
|
|
12
13
|
@file_watcher = file_watcher
|
|
13
14
|
@config = config
|
|
14
15
|
@router = Router.new(docs_path: docs_path)
|
|
15
|
-
@renderer = Renderer.new
|
|
16
|
+
@renderer = Renderer.new(base_url: config&.build&.base_url || "/")
|
|
16
17
|
@asset_handler = AssetHandler.new
|
|
17
18
|
end
|
|
18
19
|
|
|
@@ -46,9 +47,12 @@ module Docyard
|
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def render_documentation_page(file_path, current_path)
|
|
50
|
+
sidebar_builder = build_sidebar_instance(current_path)
|
|
51
|
+
|
|
49
52
|
html = renderer.render_file(
|
|
50
53
|
file_path,
|
|
51
|
-
sidebar_html:
|
|
54
|
+
sidebar_html: sidebar_builder.to_html,
|
|
55
|
+
prev_next_html: build_prev_next(sidebar_builder, current_path, file_path),
|
|
52
56
|
branding: branding_options
|
|
53
57
|
)
|
|
54
58
|
|
|
@@ -60,14 +64,32 @@ module Docyard
|
|
|
60
64
|
[Constants::STATUS_NOT_FOUND, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
|
|
61
65
|
end
|
|
62
66
|
|
|
63
|
-
def
|
|
67
|
+
def build_sidebar_instance(current_path)
|
|
64
68
|
SidebarBuilder.new(
|
|
65
69
|
docs_path: docs_path,
|
|
66
70
|
current_path: current_path,
|
|
67
71
|
config: config
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_prev_next(sidebar_builder, current_path, file_path)
|
|
76
|
+
markdown_content = File.read(file_path)
|
|
77
|
+
markdown = Markdown.new(markdown_content)
|
|
78
|
+
|
|
79
|
+
PrevNextBuilder.new(
|
|
80
|
+
sidebar_tree: sidebar_builder.tree,
|
|
81
|
+
current_path: current_path,
|
|
82
|
+
frontmatter: markdown.frontmatter,
|
|
83
|
+
config: navigation_config
|
|
68
84
|
).to_html
|
|
69
85
|
end
|
|
70
86
|
|
|
87
|
+
def navigation_config
|
|
88
|
+
return {} unless config
|
|
89
|
+
|
|
90
|
+
config.navigation&.footer || {}
|
|
91
|
+
end
|
|
92
|
+
|
|
71
93
|
def branding_options
|
|
72
94
|
return default_branding unless config
|
|
73
95
|
|
data/lib/docyard/renderer.rb
CHANGED
|
@@ -9,30 +9,40 @@ module Docyard
|
|
|
9
9
|
ERRORS_PATH = File.join(__dir__, "templates", "errors")
|
|
10
10
|
PARTIALS_PATH = File.join(__dir__, "templates", "partials")
|
|
11
11
|
|
|
12
|
-
attr_reader :layout_path
|
|
12
|
+
attr_reader :layout_path, :base_url
|
|
13
13
|
|
|
14
|
-
def initialize(layout: "default")
|
|
14
|
+
def initialize(layout: "default", base_url: "/")
|
|
15
15
|
@layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
|
|
16
|
+
@base_url = normalize_base_url(base_url)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
def render_file(file_path, sidebar_html: "", branding: {})
|
|
19
|
+
def render_file(file_path, sidebar_html: "", prev_next_html: "", branding: {})
|
|
19
20
|
markdown_content = File.read(file_path)
|
|
20
21
|
markdown = Markdown.new(markdown_content)
|
|
21
22
|
|
|
22
23
|
html_content = strip_md_from_links(markdown.html)
|
|
24
|
+
toc = markdown.toc
|
|
23
25
|
|
|
24
26
|
render(
|
|
25
27
|
content: html_content,
|
|
26
28
|
page_title: markdown.title || Constants::DEFAULT_SITE_TITLE,
|
|
27
|
-
|
|
29
|
+
navigation: {
|
|
30
|
+
sidebar_html: sidebar_html,
|
|
31
|
+
prev_next_html: prev_next_html,
|
|
32
|
+
toc: toc
|
|
33
|
+
},
|
|
28
34
|
branding: branding
|
|
29
35
|
)
|
|
30
36
|
end
|
|
31
37
|
|
|
32
|
-
def render(content:, page_title: Constants::DEFAULT_SITE_TITLE,
|
|
38
|
+
def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, navigation: {}, branding: {})
|
|
33
39
|
template = File.read(layout_path)
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
sidebar_html = navigation[:sidebar_html] || ""
|
|
42
|
+
prev_next_html = navigation[:prev_next_html] || ""
|
|
43
|
+
toc = navigation[:toc] || []
|
|
44
|
+
|
|
45
|
+
assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
|
|
36
46
|
assign_branding_variables(branding)
|
|
37
47
|
|
|
38
48
|
ERB.new(template).result(binding)
|
|
@@ -66,15 +76,30 @@ module Docyard
|
|
|
66
76
|
def asset_path(path)
|
|
67
77
|
return path if path.nil? || path.start_with?("http://", "https://")
|
|
68
78
|
|
|
69
|
-
"
|
|
79
|
+
"#{base_url}#{path}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def link_path(path)
|
|
83
|
+
return path if path.nil? || path.start_with?("http://", "https://")
|
|
84
|
+
|
|
85
|
+
"#{base_url.chomp('/')}#{path}"
|
|
70
86
|
end
|
|
71
87
|
|
|
72
88
|
private
|
|
73
89
|
|
|
74
|
-
def
|
|
90
|
+
def normalize_base_url(url)
|
|
91
|
+
return "/" if url.nil? || url.empty?
|
|
92
|
+
|
|
93
|
+
url = "/#{url}" unless url.start_with?("/")
|
|
94
|
+
url.end_with?("/") ? url : "#{url}/"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
|
|
75
98
|
@content = content
|
|
76
99
|
@page_title = page_title
|
|
77
100
|
@sidebar_html = sidebar_html
|
|
101
|
+
@prev_next_html = prev_next_html
|
|
102
|
+
@toc = toc
|
|
78
103
|
end
|
|
79
104
|
|
|
80
105
|
def assign_branding_variables(branding)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "item"
|
|
4
|
+
require_relative "title_extractor"
|
|
5
|
+
|
|
6
|
+
module Docyard
|
|
7
|
+
module Sidebar
|
|
8
|
+
class ConfigParser
|
|
9
|
+
attr_reader :config_items, :docs_path, :current_path, :title_extractor
|
|
10
|
+
|
|
11
|
+
def initialize(config_items, docs_path:, current_path: "/", title_extractor: TitleExtractor.new)
|
|
12
|
+
@config_items = config_items || []
|
|
13
|
+
@docs_path = docs_path
|
|
14
|
+
@current_path = Utils::PathResolver.normalize(current_path)
|
|
15
|
+
@title_extractor = title_extractor
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse
|
|
19
|
+
parse_items(config_items)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def parse_items(items, base_path = "")
|
|
25
|
+
items.map do |item_config|
|
|
26
|
+
parse_item(item_config, base_path)
|
|
27
|
+
end.compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_item(item_config, base_path)
|
|
31
|
+
case item_config
|
|
32
|
+
when String
|
|
33
|
+
resolve_file_item(item_config, base_path)
|
|
34
|
+
when Hash
|
|
35
|
+
parse_hash_item(item_config, base_path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_hash_item(item_config, base_path)
|
|
40
|
+
return parse_link_item(item_config) if link_item?(item_config)
|
|
41
|
+
return parse_nested_item(item_config, base_path) if nested_item?(item_config)
|
|
42
|
+
return resolve_file_item(item_config.keys.first, base_path, {}) if nil_value_item?(item_config)
|
|
43
|
+
|
|
44
|
+
slug = item_config.keys.first
|
|
45
|
+
options = item_config.values.first || {}
|
|
46
|
+
resolve_file_item(slug, base_path, options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def link_item?(config)
|
|
50
|
+
config.key?("link") || config.key?(:link)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def nested_item?(config)
|
|
54
|
+
config.size == 1 && config.values.first.is_a?(Hash)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def nil_value_item?(config)
|
|
58
|
+
config.size == 1 && config.values.first.nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse_link_item(config)
|
|
62
|
+
link = config["link"] || config[:link]
|
|
63
|
+
text = config["text"] || config[:text]
|
|
64
|
+
icon = config["icon"] || config[:icon]
|
|
65
|
+
target = config["target"] || config[:target] || "_blank"
|
|
66
|
+
|
|
67
|
+
Item.new(
|
|
68
|
+
text: text,
|
|
69
|
+
link: link,
|
|
70
|
+
path: link,
|
|
71
|
+
icon: icon,
|
|
72
|
+
target: target,
|
|
73
|
+
type: :external
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_nested_item(item_config, base_path)
|
|
78
|
+
slug = item_config.keys.first.to_s
|
|
79
|
+
options = item_config.values.first || {}
|
|
80
|
+
nested_items = extract_nested_items(options)
|
|
81
|
+
|
|
82
|
+
dir_path = File.join(docs_path, base_path, slug)
|
|
83
|
+
|
|
84
|
+
if File.directory?(dir_path)
|
|
85
|
+
build_directory_item(slug, options, nested_items, base_path)
|
|
86
|
+
elsif nested_items.any?
|
|
87
|
+
build_file_with_children_item(slug, options, nested_items, base_path)
|
|
88
|
+
else
|
|
89
|
+
resolve_file_item(slug, base_path, options)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def extract_nested_items(options)
|
|
94
|
+
options["items"] || options[:items] || []
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def extract_common_options(options)
|
|
98
|
+
{
|
|
99
|
+
text: options["text"] || options[:text],
|
|
100
|
+
icon: options["icon"] || options[:icon],
|
|
101
|
+
collapsed: options["collapsed"] || options[:collapsed] || false
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_directory_item(slug, options, nested_items, base_path)
|
|
106
|
+
common_opts = extract_common_options(options)
|
|
107
|
+
new_base_path = File.join(base_path, slug)
|
|
108
|
+
parsed_items = parse_items(nested_items, new_base_path)
|
|
109
|
+
|
|
110
|
+
Item.new(
|
|
111
|
+
slug: slug,
|
|
112
|
+
text: common_opts[:text] || Utils::TextFormatter.titleize(slug),
|
|
113
|
+
icon: common_opts[:icon],
|
|
114
|
+
collapsed: common_opts[:collapsed],
|
|
115
|
+
items: parsed_items,
|
|
116
|
+
type: :directory
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def build_file_with_children_item(slug, options, nested_items, base_path)
|
|
121
|
+
common_opts = extract_common_options(options)
|
|
122
|
+
file_path = File.join(docs_path, base_path, "#{slug}.md")
|
|
123
|
+
url_path = Utils::PathResolver.to_url(File.join(base_path, slug))
|
|
124
|
+
resolved_text = common_opts[:text] || extract_file_title(file_path, slug)
|
|
125
|
+
|
|
126
|
+
Item.new(
|
|
127
|
+
slug: slug,
|
|
128
|
+
text: resolved_text,
|
|
129
|
+
path: url_path,
|
|
130
|
+
icon: common_opts[:icon],
|
|
131
|
+
collapsed: common_opts[:collapsed],
|
|
132
|
+
items: parse_items(nested_items, base_path),
|
|
133
|
+
active: current_path == url_path,
|
|
134
|
+
type: :file
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def extract_file_title(file_path, slug)
|
|
139
|
+
File.exist?(file_path) ? title_extractor.extract(file_path) : Utils::TextFormatter.titleize(slug)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resolve_file_item(slug, base_path, options = {})
|
|
143
|
+
slug_str = slug.to_s
|
|
144
|
+
options ||= {}
|
|
145
|
+
|
|
146
|
+
file_path = File.join(docs_path, base_path, "#{slug_str}.md")
|
|
147
|
+
url_path = Utils::PathResolver.to_url(File.join(base_path, slug_str))
|
|
148
|
+
|
|
149
|
+
frontmatter = extract_frontmatter_metadata(file_path)
|
|
150
|
+
text = resolve_item_text(slug_str, file_path, options, frontmatter[:text])
|
|
151
|
+
icon = resolve_item_icon(options, frontmatter[:icon])
|
|
152
|
+
final_path = options["link"] || options[:link] || url_path
|
|
153
|
+
|
|
154
|
+
Item.new(
|
|
155
|
+
slug: slug_str, text: text, path: final_path, icon: icon,
|
|
156
|
+
active: current_path == final_path, type: :file
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def extract_frontmatter_metadata(file_path)
|
|
161
|
+
return { text: nil, icon: nil } unless File.exist?(file_path)
|
|
162
|
+
|
|
163
|
+
markdown = Markdown.new(File.read(file_path))
|
|
164
|
+
{
|
|
165
|
+
text: markdown.sidebar_text || markdown.title,
|
|
166
|
+
icon: markdown.sidebar_icon
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def resolve_item_text(slug, file_path, options, frontmatter_text)
|
|
171
|
+
text = options["text"] || options[:text] || frontmatter_text
|
|
172
|
+
text || extract_file_title(file_path, slug)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def resolve_item_icon(options, frontmatter_icon)
|
|
176
|
+
options["icon"] || options[:icon] || frontmatter_icon
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Sidebar
|
|
5
|
+
class Item
|
|
6
|
+
attr_reader :slug, :text, :icon, :link, :target, :collapsed, :items, :path, :active, :type
|
|
7
|
+
|
|
8
|
+
def initialize(**options)
|
|
9
|
+
@slug = options[:slug]
|
|
10
|
+
@text = options[:text]
|
|
11
|
+
@icon = options[:icon]
|
|
12
|
+
@link = options[:link]
|
|
13
|
+
@target = options[:target] || "_self"
|
|
14
|
+
@collapsed = options[:collapsed] || false
|
|
15
|
+
@items = options[:items] || []
|
|
16
|
+
@path = options[:path] || options[:link]
|
|
17
|
+
@active = options[:active] || false
|
|
18
|
+
@type = options[:type] || :file
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def external?
|
|
22
|
+
return false if path.nil?
|
|
23
|
+
|
|
24
|
+
path.start_with?("http://", "https://")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def children?
|
|
28
|
+
items.any?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def title
|
|
32
|
+
text
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def children
|
|
36
|
+
items
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def collapsible?
|
|
40
|
+
children?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
{
|
|
45
|
+
title: title,
|
|
46
|
+
path: path,
|
|
47
|
+
icon: icon,
|
|
48
|
+
active: active,
|
|
49
|
+
type: type,
|
|
50
|
+
collapsed: collapsed,
|
|
51
|
+
collapsible: collapsible?,
|
|
52
|
+
target: target,
|
|
53
|
+
children: children.map(&:to_h)
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -7,10 +7,11 @@ module Docyard
|
|
|
7
7
|
class Renderer
|
|
8
8
|
PARTIALS_PATH = File.join(__dir__, "../templates/partials")
|
|
9
9
|
|
|
10
|
-
attr_reader :site_title
|
|
10
|
+
attr_reader :site_title, :base_url
|
|
11
11
|
|
|
12
|
-
def initialize(site_title: "Documentation")
|
|
12
|
+
def initialize(site_title: "Documentation", base_url: "/")
|
|
13
13
|
@site_title = site_title
|
|
14
|
+
@base_url = normalize_base_url(base_url)
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def render(tree)
|
|
@@ -34,8 +35,21 @@ module Docyard
|
|
|
34
35
|
ERB.new(template).result(erb_binding)
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
def icon(name)
|
|
38
|
-
|
|
38
|
+
def icon(name, weight = "regular")
|
|
39
|
+
Icons.render(name.to_s.tr("_", "-"), weight) || ""
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def link_path(path)
|
|
43
|
+
return path if path.nil? || path.start_with?("http://", "https://")
|
|
44
|
+
|
|
45
|
+
"#{base_url.chomp('/')}#{path}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def normalize_base_url(url)
|
|
49
|
+
return "/" if url.nil? || url.empty?
|
|
50
|
+
|
|
51
|
+
url = "/#{url}" unless url.start_with?("/")
|
|
52
|
+
url.end_with?("/") ? url : "#{url}/"
|
|
39
53
|
end
|
|
40
54
|
|
|
41
55
|
def render_tree_with_sections(items)
|
|
@@ -98,12 +112,25 @@ module Docyard
|
|
|
98
112
|
end
|
|
99
113
|
|
|
100
114
|
def render_leaf_item(item)
|
|
101
|
-
render_partial(
|
|
115
|
+
render_partial(
|
|
116
|
+
:nav_leaf,
|
|
117
|
+
path: item[:path],
|
|
118
|
+
title: item[:title],
|
|
119
|
+
active: item[:active],
|
|
120
|
+
icon: item[:icon],
|
|
121
|
+
target: item[:target]
|
|
122
|
+
)
|
|
102
123
|
end
|
|
103
124
|
|
|
104
125
|
def render_group_item(item)
|
|
105
126
|
children_html = render_tree(item[:children])
|
|
106
|
-
render_partial(
|
|
127
|
+
render_partial(
|
|
128
|
+
:nav_group,
|
|
129
|
+
title: item[:title],
|
|
130
|
+
children_html: children_html,
|
|
131
|
+
icon: item[:icon],
|
|
132
|
+
collapsed: item[:collapsed]
|
|
133
|
+
)
|
|
107
134
|
end
|
|
108
135
|
end
|
|
109
136
|
end
|
|
@@ -4,6 +4,7 @@ require_relative "sidebar/file_system_scanner"
|
|
|
4
4
|
require_relative "sidebar/title_extractor"
|
|
5
5
|
require_relative "sidebar/tree_builder"
|
|
6
6
|
require_relative "sidebar/renderer"
|
|
7
|
+
require_relative "sidebar/config_parser"
|
|
7
8
|
|
|
8
9
|
module Docyard
|
|
9
10
|
class SidebarBuilder
|
|
@@ -26,10 +27,44 @@ module Docyard
|
|
|
26
27
|
private
|
|
27
28
|
|
|
28
29
|
def build_tree
|
|
30
|
+
if config_sidebar_items?
|
|
31
|
+
build_tree_from_config
|
|
32
|
+
else
|
|
33
|
+
build_tree_from_filesystem
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_tree_from_config
|
|
38
|
+
config_parser.parse.map(&:to_h)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_tree_from_filesystem
|
|
29
42
|
file_items = scanner.scan
|
|
30
43
|
tree_builder.build(file_items)
|
|
31
44
|
end
|
|
32
45
|
|
|
46
|
+
def config_sidebar_items?
|
|
47
|
+
config_sidebar_items&.any?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def config_sidebar_items
|
|
51
|
+
return [] unless config
|
|
52
|
+
|
|
53
|
+
if config.is_a?(Hash)
|
|
54
|
+
config.dig("sidebar", "items") || config.dig(:sidebar, :items) || []
|
|
55
|
+
else
|
|
56
|
+
config.sidebar&.items || []
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def config_parser
|
|
61
|
+
@config_parser ||= Sidebar::ConfigParser.new(
|
|
62
|
+
config_sidebar_items,
|
|
63
|
+
docs_path: docs_path,
|
|
64
|
+
current_path: current_path
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
33
68
|
def scanner
|
|
34
69
|
@scanner ||= Sidebar::FileSystemScanner.new(docs_path)
|
|
35
70
|
end
|
|
@@ -43,10 +78,19 @@ module Docyard
|
|
|
43
78
|
|
|
44
79
|
def renderer
|
|
45
80
|
@renderer ||= Sidebar::Renderer.new(
|
|
46
|
-
site_title: extract_site_title
|
|
81
|
+
site_title: extract_site_title,
|
|
82
|
+
base_url: extract_base_url
|
|
47
83
|
)
|
|
48
84
|
end
|
|
49
85
|
|
|
86
|
+
def extract_base_url
|
|
87
|
+
if config.is_a?(Hash)
|
|
88
|
+
config.dig(:build, :base_url) || "/"
|
|
89
|
+
else
|
|
90
|
+
config&.build&.base_url || "/"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
50
94
|
def extract_site_title
|
|
51
95
|
if config.is_a?(Hash)
|
|
52
96
|
config[:site_title] || "Documentation"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
background-color: var(--color-bg);
|
|
48
48
|
color: var(--color-text-secondary);
|
|
49
49
|
cursor: pointer;
|
|
50
|
-
transition:
|
|
50
|
+
transition: background-color var(--transition-base), border-color var(--transition-base), color var(--transition-base), opacity var(--transition-base), visibility var(--transition-base);
|
|
51
51
|
opacity: 0;
|
|
52
52
|
visibility: hidden;
|
|
53
53
|
}
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
.docyard-code-block__copy svg {
|
|
79
79
|
width: 16px;
|
|
80
80
|
height: 16px;
|
|
81
|
-
transition:
|
|
81
|
+
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/* Success state */
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
.content h2[id],
|
|
2
|
+
.content h3[id],
|
|
3
|
+
.content h4[id],
|
|
4
|
+
.content h5[id],
|
|
5
|
+
.content h6[id] {
|
|
6
|
+
position: relative;
|
|
7
|
+
scroll-margin-top: 100px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Tablet: Account for both primary + secondary header */
|
|
11
|
+
@media (max-width: 1280px) and (min-width: 1025px) {
|
|
12
|
+
.content h2[id],
|
|
13
|
+
.content h3[id],
|
|
14
|
+
.content h4[id],
|
|
15
|
+
.content h5[id],
|
|
16
|
+
.content h6[id] {
|
|
17
|
+
scroll-margin-top: calc(var(--header-height) + 3rem + var(--space-4));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.heading-anchor {
|
|
22
|
+
float: left;
|
|
23
|
+
margin-left: -0.75em;
|
|
24
|
+
padding-right: 0.25em;
|
|
25
|
+
font-weight: var(--font-weight-normal);
|
|
26
|
+
color: var(--color-primary);
|
|
27
|
+
text-decoration: none !important;
|
|
28
|
+
opacity: 0;
|
|
29
|
+
transition: opacity var(--transition-fast);
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
user-select: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.heading-anchor:hover,
|
|
35
|
+
.heading-anchor:focus {
|
|
36
|
+
opacity: 1;
|
|
37
|
+
text-decoration: none !important;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.heading-anchor:active,
|
|
41
|
+
.heading-anchor:visited {
|
|
42
|
+
text-decoration: none !important;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.heading-anchor:focus {
|
|
46
|
+
outline: 2px solid var(--color-primary);
|
|
47
|
+
outline-offset: 2px;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.content h2[id]:hover .heading-anchor,
|
|
52
|
+
.content h3[id]:hover .heading-anchor,
|
|
53
|
+
.content h4[id]:hover .heading-anchor,
|
|
54
|
+
.content h5[id]:hover .heading-anchor,
|
|
55
|
+
.content h6[id]:hover .heading-anchor {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (max-width: 1024px) {
|
|
60
|
+
.heading-anchor {
|
|
61
|
+
position: static;
|
|
62
|
+
margin-left: var(--space-2);
|
|
63
|
+
float: none;
|
|
64
|
+
opacity: 0.6;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.heading-anchor:hover,
|
|
68
|
+
.heading-anchor:focus {
|
|
69
|
+
opacity: 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@media (prefers-reduced-motion: reduce) {
|
|
74
|
+
.heading-anchor {
|
|
75
|
+
transition: none;
|
|
76
|
+
}
|
|
77
|
+
}
|