docyard 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +34 -1
- data/lib/docyard/build/asset_bundler.rb +22 -7
- data/lib/docyard/build/file_copier.rb +49 -27
- data/lib/docyard/build/sitemap_generator.rb +6 -6
- data/lib/docyard/build/static_generator.rb +82 -50
- data/lib/docyard/builder.rb +20 -10
- 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 +183 -0
- data/lib/docyard/{constants.rb → config/constants.rb} +7 -4
- data/lib/docyard/config/validator.rb +122 -99
- data/lib/docyard/config.rb +38 -36
- data/lib/docyard/initializer.rb +15 -76
- data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
- data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +6 -3
- data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
- data/lib/docyard/navigation/sidebar/config_parser.rb +208 -0
- data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
- data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +2 -1
- data/lib/docyard/navigation/sidebar/item.rb +96 -0
- data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
- data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
- data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
- data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
- data/lib/docyard/navigation/sidebar/renderer.rb +144 -0
- data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
- data/lib/docyard/navigation/sidebar/tree_builder.rb +139 -0
- data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
- data/lib/docyard/navigation/sidebar_builder.rb +159 -0
- data/lib/docyard/rendering/icon_helpers.rb +13 -0
- data/lib/docyard/{icons → rendering/icons}/phosphor.rb +26 -1
- data/lib/docyard/{markdown.rb → rendering/markdown.rb} +19 -13
- data/lib/docyard/rendering/renderer.rb +163 -0
- data/lib/docyard/rendering/template_resolver.rb +172 -0
- data/lib/docyard/routing/fallback_resolver.rb +92 -0
- data/lib/docyard/search/build_indexer.rb +74 -0
- data/lib/docyard/search/dev_indexer.rb +155 -0
- data/lib/docyard/search/pagefind_support.rb +33 -0
- data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +24 -19
- data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
- data/lib/docyard/server/pagefind_handler.rb +63 -0
- data/lib/docyard/{preview_server.rb → server/preview_server.rb} +2 -2
- data/lib/docyard/server/rack_application.rb +192 -0
- 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/code.css +18 -51
- data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
- data/lib/docyard/templates/assets/css/components/callout.css +67 -67
- data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
- data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
- data/lib/docyard/templates/assets/css/components/icon.css +0 -1
- data/lib/docyard/templates/assets/css/components/logo.css +0 -2
- data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
- data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
- data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
- data/lib/docyard/templates/assets/css/components/search.css +561 -0
- data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
- data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
- data/lib/docyard/templates/assets/css/landing.css +815 -0
- data/lib/docyard/templates/assets/css/layout.css +503 -87
- data/lib/docyard/templates/assets/css/main.css +1 -3
- data/lib/docyard/templates/assets/css/markdown.css +111 -93
- data/lib/docyard/templates/assets/css/reset.css +0 -3
- data/lib/docyard/templates/assets/css/typography.css +43 -41
- data/lib/docyard/templates/assets/css/variables.css +268 -208
- data/lib/docyard/templates/assets/favicon.svg +7 -8
- data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
- data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
- data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
- data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
- data/lib/docyard/templates/assets/js/components/search.js +610 -0
- data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
- data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
- data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
- data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
- data/lib/docyard/templates/assets/js/theme.js +0 -3
- data/lib/docyard/templates/assets/logo-dark.svg +8 -2
- data/lib/docyard/templates/assets/logo.svg +7 -4
- data/lib/docyard/templates/config/docyard.yml.erb +37 -34
- data/lib/docyard/templates/errors/404.html.erb +1 -1
- data/lib/docyard/templates/errors/500.html.erb +1 -1
- data/lib/docyard/templates/layouts/default.html.erb +19 -56
- data/lib/docyard/templates/layouts/splash.html.erb +176 -0
- data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
- data/lib/docyard/templates/partials/_code_block.html.erb +6 -4
- data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
- data/lib/docyard/templates/partials/_features.html.erb +15 -0
- data/lib/docyard/templates/partials/_footer.html.erb +42 -0
- data/lib/docyard/templates/partials/_head.html.erb +22 -0
- data/lib/docyard/templates/partials/_header.html.erb +49 -0
- data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
- data/lib/docyard/templates/partials/_hero.html.erb +27 -0
- data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
- data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
- data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
- data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
- data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
- data/lib/docyard/templates/partials/_prev_next.html.erb +9 -3
- data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
- data/lib/docyard/templates/partials/_search_modal.html.erb +41 -0
- data/lib/docyard/templates/partials/_search_trigger.html.erb +18 -0
- data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
- data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
- data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
- data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
- data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
- data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
- 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 +89 -50
- 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/rack_application.rb +0 -172
- data/lib/docyard/renderer.rb +0 -120
- data/lib/docyard/routing/resolution_result.rb +0 -31
- data/lib/docyard/sidebar/config_parser.rb +0 -180
- data/lib/docyard/sidebar/item.rb +0 -58
- data/lib/docyard/sidebar/renderer.rb +0 -137
- data/lib/docyard/sidebar/tree_builder.rb +0 -59
- data/lib/docyard/sidebar_builder.rb +0 -102
- data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
- data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
- data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
- data/lib/docyard/templates/markdown/index.md.erb +0 -82
- /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,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require_relative "../config/constants"
|
|
5
|
+
require_relative "icon_helpers"
|
|
6
|
+
|
|
7
|
+
module Docyard
|
|
8
|
+
class Renderer
|
|
9
|
+
include Utils::UrlHelpers
|
|
10
|
+
include IconHelpers
|
|
11
|
+
|
|
12
|
+
LAYOUTS_PATH = File.join(__dir__, "../templates", "layouts")
|
|
13
|
+
ERRORS_PATH = File.join(__dir__, "../templates", "errors")
|
|
14
|
+
PARTIALS_PATH = File.join(__dir__, "../templates", "partials")
|
|
15
|
+
DEFAULT_LAYOUT = "default"
|
|
16
|
+
|
|
17
|
+
attr_reader :base_url, :config
|
|
18
|
+
|
|
19
|
+
def initialize(base_url: "/", config: nil)
|
|
20
|
+
@base_url = normalize_base_url(base_url)
|
|
21
|
+
@config = config
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def render_file(file_path, sidebar_html: "", prev_next_html: "", breadcrumbs: nil, branding: {},
|
|
25
|
+
template_options: {}, current_path: "/")
|
|
26
|
+
markdown = Markdown.new(File.read(file_path), config: config)
|
|
27
|
+
|
|
28
|
+
render(
|
|
29
|
+
content: strip_md_from_links(markdown.html),
|
|
30
|
+
page_title: markdown.title || Constants::DEFAULT_SITE_TITLE,
|
|
31
|
+
navigation: build_navigation(sidebar_html, prev_next_html, markdown.toc, breadcrumbs),
|
|
32
|
+
branding: branding,
|
|
33
|
+
template_options: template_options,
|
|
34
|
+
current_path: current_path
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_navigation(sidebar_html, prev_next_html, toc, breadcrumbs)
|
|
39
|
+
{ sidebar_html: sidebar_html, prev_next_html: prev_next_html, toc: toc, breadcrumbs: breadcrumbs }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, navigation: {}, branding: {},
|
|
43
|
+
template_options: {}, current_path: "/")
|
|
44
|
+
layout = template_options[:template] || DEFAULT_LAYOUT
|
|
45
|
+
layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
|
|
46
|
+
template = File.read(layout_path)
|
|
47
|
+
|
|
48
|
+
assign_content_variables(content, page_title, navigation)
|
|
49
|
+
assign_branding_variables(branding, current_path)
|
|
50
|
+
assign_template_variables(template_options)
|
|
51
|
+
|
|
52
|
+
ERB.new(template).result(binding)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_not_found
|
|
56
|
+
render_error_template(404)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_server_error(error)
|
|
60
|
+
@error_message = error.message
|
|
61
|
+
@backtrace = error.backtrace.join("\n")
|
|
62
|
+
render_error_template(500)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_error_template(status)
|
|
66
|
+
error_template_path = File.join(ERRORS_PATH, "#{status}.html.erb")
|
|
67
|
+
template = File.read(error_template_path)
|
|
68
|
+
ERB.new(template).result(binding)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render_partial(name, locals = {})
|
|
72
|
+
partial_path = File.join(PARTIALS_PATH, "#{name}.html.erb")
|
|
73
|
+
template = File.read(partial_path)
|
|
74
|
+
|
|
75
|
+
locals.each { |key, value| instance_variable_set("@#{key}", value) }
|
|
76
|
+
|
|
77
|
+
ERB.new(template).result(binding)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def asset_path(path)
|
|
81
|
+
return path if path.nil? || path.start_with?("http://", "https://")
|
|
82
|
+
|
|
83
|
+
"#{base_url}#{path}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def assign_content_variables(content, page_title, navigation)
|
|
89
|
+
@content = content
|
|
90
|
+
@page_title = page_title
|
|
91
|
+
@sidebar_html = navigation[:sidebar_html] || ""
|
|
92
|
+
@prev_next_html = navigation[:prev_next_html] || ""
|
|
93
|
+
@toc = navigation[:toc] || []
|
|
94
|
+
@breadcrumbs = navigation[:breadcrumbs]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def assign_branding_variables(branding, current_path = "/")
|
|
98
|
+
assign_site_branding(branding)
|
|
99
|
+
assign_search_options(branding)
|
|
100
|
+
assign_credits_and_social(branding)
|
|
101
|
+
assign_tabs(branding, current_path)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def assign_site_branding(branding)
|
|
105
|
+
@site_title = branding[:site_title] || Constants::DEFAULT_SITE_TITLE
|
|
106
|
+
@site_description = branding[:site_description] || ""
|
|
107
|
+
@logo = branding[:logo] || Constants::DEFAULT_LOGO_PATH
|
|
108
|
+
@logo_dark = branding[:logo_dark]
|
|
109
|
+
@favicon = branding[:favicon] || Constants::DEFAULT_FAVICON_PATH
|
|
110
|
+
@has_custom_logo = branding[:has_custom_logo] || false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def assign_search_options(branding)
|
|
114
|
+
@search_enabled = branding[:search_enabled].nil? || branding[:search_enabled]
|
|
115
|
+
@search_placeholder = branding[:search_placeholder] || "Search documentation..."
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def assign_credits_and_social(branding)
|
|
119
|
+
@credits = branding[:credits] != false
|
|
120
|
+
@copyright = branding[:copyright]
|
|
121
|
+
@social = branding[:social] || []
|
|
122
|
+
@header_ctas = branding[:header_ctas] || []
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def assign_tabs(branding, current_path)
|
|
126
|
+
tabs = branding[:tabs] || []
|
|
127
|
+
@tabs = tabs.map { |tab| tab.merge(active: tab_active?(tab[:href], current_path)) }
|
|
128
|
+
@has_tabs = branding[:has_tabs] || false
|
|
129
|
+
@current_path = current_path
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def tab_active?(tab_href, current_path)
|
|
133
|
+
return false if tab_href.nil? || current_path.nil?
|
|
134
|
+
return false if tab_href.start_with?("http://", "https://")
|
|
135
|
+
|
|
136
|
+
normalized_tab = tab_href.chomp("/")
|
|
137
|
+
normalized_current = current_path.chomp("/")
|
|
138
|
+
|
|
139
|
+
return true if normalized_tab == normalized_current
|
|
140
|
+
|
|
141
|
+
current_path.start_with?("#{normalized_tab}/")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def assign_template_variables(template_options)
|
|
145
|
+
@hero = template_options[:hero]
|
|
146
|
+
@features = template_options[:features]
|
|
147
|
+
@features_header = template_options[:features_header]
|
|
148
|
+
@show_sidebar = template_options.fetch(:show_sidebar, true)
|
|
149
|
+
@show_toc = template_options.fetch(:show_toc, true)
|
|
150
|
+
assign_footer_from_landing(template_options[:footer])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def assign_footer_from_landing(footer)
|
|
154
|
+
return unless footer
|
|
155
|
+
|
|
156
|
+
@footer_links = footer[:links]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def strip_md_from_links(html)
|
|
160
|
+
html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class TemplateResolver
|
|
5
|
+
BACKGROUNDS = %w[grid glow mesh none].freeze
|
|
6
|
+
DEFAULT_BACKGROUND = "grid"
|
|
7
|
+
|
|
8
|
+
attr_reader :frontmatter, :site_config
|
|
9
|
+
|
|
10
|
+
def initialize(frontmatter, site_config = {})
|
|
11
|
+
@frontmatter = frontmatter || {}
|
|
12
|
+
@site_config = site_config || {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def landing?
|
|
16
|
+
landing_config.any?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def template
|
|
20
|
+
landing? ? "splash" : "default"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def show_sidebar?
|
|
24
|
+
if landing?
|
|
25
|
+
landing_config.fetch("sidebar", false)
|
|
26
|
+
else
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def show_toc?
|
|
32
|
+
return false if landing?
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def hero_config
|
|
38
|
+
return nil unless landing?
|
|
39
|
+
|
|
40
|
+
hero = landing_config["hero"]
|
|
41
|
+
return nil unless hero.is_a?(Hash)
|
|
42
|
+
|
|
43
|
+
symbolize_hero(hero)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def features_config
|
|
47
|
+
return nil unless landing?
|
|
48
|
+
|
|
49
|
+
features = landing_config["features"]
|
|
50
|
+
return nil unless features.is_a?(Array)
|
|
51
|
+
|
|
52
|
+
features.map { |f| symbolize_feature(f) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def features_header_config
|
|
56
|
+
return nil unless landing?
|
|
57
|
+
|
|
58
|
+
header = landing_config["features_header"]
|
|
59
|
+
return nil unless header.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
label: header["label"],
|
|
63
|
+
title: header["title"],
|
|
64
|
+
description: header["description"]
|
|
65
|
+
}.compact
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def footer_config
|
|
69
|
+
return nil unless landing?
|
|
70
|
+
|
|
71
|
+
footer = landing_config["footer"]
|
|
72
|
+
return nil unless footer.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
links: normalize_footer_links(footer["links"])
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_options
|
|
80
|
+
{
|
|
81
|
+
template: template,
|
|
82
|
+
landing: landing?,
|
|
83
|
+
show_sidebar: show_sidebar?,
|
|
84
|
+
show_toc: show_toc?,
|
|
85
|
+
hero: hero_config,
|
|
86
|
+
features: features_config,
|
|
87
|
+
features_header: features_header_config,
|
|
88
|
+
footer: footer_config
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def normalize_footer_links(links)
|
|
95
|
+
return nil unless links.is_a?(Array)
|
|
96
|
+
|
|
97
|
+
links.map do |link|
|
|
98
|
+
next unless link.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
{ text: link["text"], link: link["link"] }
|
|
101
|
+
end.compact
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def landing_config
|
|
105
|
+
@landing_config ||= frontmatter["landing"] || site_config["landing"] || {}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def symbolize_hero(hero)
|
|
109
|
+
background = hero["background"]
|
|
110
|
+
validated_bg = BACKGROUNDS.include?(background) ? background : DEFAULT_BACKGROUND
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
background: validated_bg,
|
|
114
|
+
badge: hero["badge"],
|
|
115
|
+
name: hero["name"],
|
|
116
|
+
title: hero["title"],
|
|
117
|
+
tagline: hero["tagline"],
|
|
118
|
+
gradient: hero.fetch("gradient", true),
|
|
119
|
+
image: symbolize_image(hero["image"]),
|
|
120
|
+
actions: symbolize_actions(hero["actions"])
|
|
121
|
+
}.compact
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def symbolize_image(image)
|
|
125
|
+
return nil unless image.is_a?(Hash)
|
|
126
|
+
|
|
127
|
+
if image["light"] || image["dark"]
|
|
128
|
+
{
|
|
129
|
+
light: image["light"],
|
|
130
|
+
dark: image["dark"],
|
|
131
|
+
alt: image["alt"]
|
|
132
|
+
}.compact
|
|
133
|
+
else
|
|
134
|
+
{
|
|
135
|
+
src: image["src"],
|
|
136
|
+
alt: image["alt"]
|
|
137
|
+
}.compact
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def symbolize_actions(actions)
|
|
142
|
+
return nil unless actions.is_a?(Array)
|
|
143
|
+
|
|
144
|
+
actions.map do |action|
|
|
145
|
+
{
|
|
146
|
+
text: action["text"],
|
|
147
|
+
link: action["link"],
|
|
148
|
+
icon: action["icon"],
|
|
149
|
+
variant: action["variant"] || "primary",
|
|
150
|
+
target: action["target"],
|
|
151
|
+
rel: action["rel"]
|
|
152
|
+
}.compact
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def symbolize_feature(feature)
|
|
157
|
+
return {} unless feature.is_a?(Hash)
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
title: feature["title"],
|
|
161
|
+
description: feature["description"],
|
|
162
|
+
icon: feature["icon"],
|
|
163
|
+
color: feature["color"],
|
|
164
|
+
link: feature["link"],
|
|
165
|
+
link_text: feature["link_text"],
|
|
166
|
+
size: feature["size"],
|
|
167
|
+
target: feature["target"],
|
|
168
|
+
rel: feature["rel"]
|
|
169
|
+
}.compact
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Routing
|
|
5
|
+
class FallbackResolver
|
|
6
|
+
attr_reader :docs_path, :sidebar_builder
|
|
7
|
+
|
|
8
|
+
def initialize(docs_path:, sidebar_builder:)
|
|
9
|
+
@docs_path = docs_path
|
|
10
|
+
@sidebar_builder = sidebar_builder
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve_fallback(request_path)
|
|
14
|
+
return nil if file_exists?(request_path)
|
|
15
|
+
|
|
16
|
+
find_first_item_in_section(request_path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def file_exists?(request_path)
|
|
22
|
+
clean_path = sanitize_path(request_path)
|
|
23
|
+
|
|
24
|
+
file_path = File.join(docs_path, "#{clean_path}.md")
|
|
25
|
+
return true if File.file?(file_path)
|
|
26
|
+
|
|
27
|
+
index_path = File.join(docs_path, clean_path, "index.md")
|
|
28
|
+
File.file?(index_path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sanitize_path(request_path)
|
|
32
|
+
clean = request_path.to_s.delete_prefix("/").delete_suffix("/")
|
|
33
|
+
clean = "index" if clean.empty?
|
|
34
|
+
clean.delete_suffix(".md")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_first_item_in_section(request_path)
|
|
38
|
+
tree = sidebar_builder.tree
|
|
39
|
+
|
|
40
|
+
if root_path?(request_path)
|
|
41
|
+
find_first_navigable_item(tree)
|
|
42
|
+
else
|
|
43
|
+
section = find_section_in_tree(tree, request_path)
|
|
44
|
+
section ? find_first_navigable_item(section[:children] || []) : nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def root_path?(request_path)
|
|
49
|
+
request_path.nil? || request_path == "/" || request_path.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_section_in_tree(tree, path)
|
|
53
|
+
normalized_path = normalize_path(path)
|
|
54
|
+
|
|
55
|
+
tree.each do |item|
|
|
56
|
+
return item if path_matches_section?(item, normalized_path)
|
|
57
|
+
|
|
58
|
+
if item[:children]&.any?
|
|
59
|
+
found = find_section_in_tree(item[:children], path)
|
|
60
|
+
return found if found
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def normalize_path(path)
|
|
68
|
+
path.to_s.delete_prefix("/").delete_suffix("/").downcase
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def path_matches_section?(item, normalized_path)
|
|
72
|
+
return false unless item[:type] == :directory
|
|
73
|
+
|
|
74
|
+
item_path = item[:title].to_s.downcase.gsub(/\s+/, "-")
|
|
75
|
+
item_path == normalized_path
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def find_first_navigable_item(items)
|
|
79
|
+
items.each do |item|
|
|
80
|
+
return item[:path] if item[:path] && item[:type] == :file
|
|
81
|
+
|
|
82
|
+
if item[:children]&.any?
|
|
83
|
+
found = find_first_navigable_item(item[:children])
|
|
84
|
+
return found if found
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Search
|
|
7
|
+
class BuildIndexer
|
|
8
|
+
include PagefindSupport
|
|
9
|
+
|
|
10
|
+
PAGEFIND_COMMAND = "npx"
|
|
11
|
+
|
|
12
|
+
attr_reader :config, :output_dir, :verbose
|
|
13
|
+
|
|
14
|
+
def initialize(config, verbose: false)
|
|
15
|
+
@config = config
|
|
16
|
+
@output_dir = config.build.output
|
|
17
|
+
@verbose = verbose
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def index
|
|
21
|
+
return 0 unless search_enabled?
|
|
22
|
+
|
|
23
|
+
log "Generating search index..."
|
|
24
|
+
|
|
25
|
+
unless pagefind_available?
|
|
26
|
+
warn_pagefind_missing
|
|
27
|
+
return 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
run_pagefind
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def warn_pagefind_missing
|
|
36
|
+
log_warning "[!] Search index skipped: Pagefind not found"
|
|
37
|
+
log_warning " Install with: npm install -g pagefind"
|
|
38
|
+
log_warning " Or run: npx pagefind --site #{output_dir}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run_pagefind
|
|
42
|
+
args = build_pagefind_args(output_dir)
|
|
43
|
+
log "Running: npx #{args.join(' ')}" if verbose
|
|
44
|
+
|
|
45
|
+
stdout, stderr, status = Open3.capture3(PAGEFIND_COMMAND, *args)
|
|
46
|
+
|
|
47
|
+
if status.success?
|
|
48
|
+
page_count = extract_page_count(stdout)
|
|
49
|
+
log "[+] Generated search index (#{page_count} pages indexed)"
|
|
50
|
+
page_count
|
|
51
|
+
else
|
|
52
|
+
log_warning "[!] Search indexing failed: #{stderr}"
|
|
53
|
+
0
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extract_page_count(output)
|
|
58
|
+
if output =~ /Indexed (\d+) page/i
|
|
59
|
+
Regexp.last_match(1).to_i
|
|
60
|
+
else
|
|
61
|
+
0
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def log(message)
|
|
66
|
+
puts message
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def log_warning(message)
|
|
70
|
+
warn message
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "tty-progressbar"
|
|
7
|
+
|
|
8
|
+
module Docyard
|
|
9
|
+
module Search
|
|
10
|
+
class DevIndexer
|
|
11
|
+
include PagefindSupport
|
|
12
|
+
|
|
13
|
+
attr_reader :docs_path, :config, :temp_dir, :pagefind_path
|
|
14
|
+
|
|
15
|
+
def initialize(docs_path:, config:)
|
|
16
|
+
@docs_path = docs_path
|
|
17
|
+
@config = config
|
|
18
|
+
@temp_dir = nil
|
|
19
|
+
@pagefind_path = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate
|
|
23
|
+
return unless search_enabled?
|
|
24
|
+
return unless pagefind_available?
|
|
25
|
+
|
|
26
|
+
@temp_dir = Dir.mktmpdir("docyard-search-")
|
|
27
|
+
generate_html_files
|
|
28
|
+
page_count = run_pagefind
|
|
29
|
+
@pagefind_path = File.join(temp_dir, "pagefind")
|
|
30
|
+
|
|
31
|
+
log_success(page_count)
|
|
32
|
+
pagefind_path
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
warn "[!] Search index generation failed: #{e.message}"
|
|
35
|
+
cleanup
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def cleanup
|
|
40
|
+
return unless temp_dir && Dir.exist?(temp_dir)
|
|
41
|
+
|
|
42
|
+
FileUtils.rm_rf(temp_dir)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def pagefind_available?
|
|
48
|
+
result = super
|
|
49
|
+
warn "[!] Search disabled: Pagefind not found (npm install -g pagefind)" unless result
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate_html_files
|
|
54
|
+
markdown_files = Dir.glob(File.join(docs_path, "**", "*.md"))
|
|
55
|
+
markdown_files = filter_excluded_files(markdown_files)
|
|
56
|
+
markdown_files = filter_non_indexable_files(markdown_files)
|
|
57
|
+
renderer = Renderer.new(base_url: "/", config: config)
|
|
58
|
+
|
|
59
|
+
progress = TTY::ProgressBar.new(
|
|
60
|
+
"Indexing search [:bar] :current/:total (:percent)",
|
|
61
|
+
total: markdown_files.size,
|
|
62
|
+
width: 50
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
markdown_files.each do |file_path|
|
|
66
|
+
generate_html_file(file_path, renderer)
|
|
67
|
+
progress.advance
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def filter_excluded_files(files)
|
|
72
|
+
exclude_patterns = config.search.exclude || []
|
|
73
|
+
return files if exclude_patterns.empty?
|
|
74
|
+
|
|
75
|
+
files.reject do |file_path|
|
|
76
|
+
url_path = file_to_url_path(file_path)
|
|
77
|
+
exclude_patterns.any? { |pattern| File.fnmatch(pattern, url_path, File::FNM_PATHNAME) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def filter_non_indexable_files(files)
|
|
82
|
+
files.reject do |file_path|
|
|
83
|
+
content = File.read(file_path)
|
|
84
|
+
markdown = Markdown.new(content)
|
|
85
|
+
frontmatter = markdown.frontmatter
|
|
86
|
+
|
|
87
|
+
uses_splash_template?(frontmatter)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def uses_splash_template?(frontmatter)
|
|
92
|
+
return true if frontmatter["template"] == "splash"
|
|
93
|
+
return true if frontmatter.key?("landing")
|
|
94
|
+
|
|
95
|
+
frontmatter.key?("hero") || frontmatter.key?("features")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def file_to_url_path(file_path)
|
|
99
|
+
relative_path = file_path.delete_prefix("#{docs_path}/")
|
|
100
|
+
base_name = File.basename(relative_path, ".md")
|
|
101
|
+
dir_name = File.dirname(relative_path)
|
|
102
|
+
|
|
103
|
+
if base_name == "index"
|
|
104
|
+
dir_name == "." ? "/" : "/#{dir_name}"
|
|
105
|
+
else
|
|
106
|
+
dir_name == "." ? "/#{base_name}" : "/#{dir_name}/#{base_name}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def generate_html_file(markdown_file, renderer)
|
|
111
|
+
relative_path = markdown_file.delete_prefix("#{docs_path}/")
|
|
112
|
+
output_path = determine_output_path(relative_path)
|
|
113
|
+
|
|
114
|
+
html = renderer.render_file(markdown_file, branding: branding_options)
|
|
115
|
+
|
|
116
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
117
|
+
File.write(output_path, html)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def determine_output_path(relative_path)
|
|
121
|
+
base_name = File.basename(relative_path, ".md")
|
|
122
|
+
dir_name = File.dirname(relative_path)
|
|
123
|
+
|
|
124
|
+
if base_name == "index"
|
|
125
|
+
File.join(temp_dir, dir_name, "index.html")
|
|
126
|
+
else
|
|
127
|
+
File.join(temp_dir, dir_name, base_name, "index.html")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def branding_options
|
|
132
|
+
BrandingResolver.new(config).resolve
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def run_pagefind
|
|
136
|
+
args = build_pagefind_args(temp_dir)
|
|
137
|
+
stdout, stderr, status = Open3.capture3("npx", *args)
|
|
138
|
+
|
|
139
|
+
raise "Pagefind failed: #{stderr}" unless status.success?
|
|
140
|
+
|
|
141
|
+
extract_page_count(stdout)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def extract_page_count(output)
|
|
145
|
+
match = output.match(/Indexed (\d+) page/i)
|
|
146
|
+
match ? match[1].to_i : 0
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def log_success(page_count)
|
|
150
|
+
puts "=> Search index generated (#{page_count} pages indexed)"
|
|
151
|
+
puts "=> Temp directory: #{temp_dir}" if ENV["DOCYARD_DEBUG"]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Search
|
|
7
|
+
module PagefindSupport
|
|
8
|
+
def search_enabled?
|
|
9
|
+
config.search.enabled != false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pagefind_available?
|
|
13
|
+
_stdout, _stderr, status = Open3.capture3("npx", "pagefind", "--version")
|
|
14
|
+
status.success?
|
|
15
|
+
rescue Errno::ENOENT
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def build_pagefind_args(site_dir)
|
|
20
|
+
args = ["pagefind", "--site", site_dir]
|
|
21
|
+
|
|
22
|
+
exclusions = config.search.exclude || []
|
|
23
|
+
exclusions.each do |pattern|
|
|
24
|
+
next if pattern.start_with?("/")
|
|
25
|
+
|
|
26
|
+
args += ["--exclude-selectors", pattern]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
args
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|