docyard 0.9.0 → 1.0.1
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 +57 -1
- data/README.md +8 -253
- data/exe/docyard +6 -0
- data/lib/docyard/build/asset_bundler.rb +24 -2
- data/lib/docyard/build/error_page_generator.rb +33 -0
- data/lib/docyard/build/file_copier.rb +12 -5
- data/lib/docyard/build/file_writer.rb +19 -0
- data/lib/docyard/build/llms_txt_generator.rb +103 -0
- data/lib/docyard/build/root_fallback_generator.rb +66 -0
- data/lib/docyard/build/sitemap_generator.rb +1 -1
- data/lib/docyard/build/static_generator.rb +119 -81
- data/lib/docyard/builder.rb +6 -2
- data/lib/docyard/cli.rb +14 -4
- data/lib/docyard/components/processors/callout_processor.rb +1 -1
- data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
- data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +11 -1
- data/lib/docyard/components/processors/code_block_processor.rb +5 -24
- data/lib/docyard/components/processors/code_group_processor.rb +6 -22
- data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +1 -0
- data/lib/docyard/components/processors/file_tree_processor.rb +1 -2
- data/lib/docyard/components/processors/icon_processor.rb +8 -2
- data/lib/docyard/components/processors/include_processor.rb +10 -10
- data/lib/docyard/components/processors/video_embed_processor.rb +14 -3
- data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
- data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
- data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
- data/lib/docyard/components/support/code_detector.rb +2 -12
- data/lib/docyard/components/support/code_group/html_builder.rb +2 -6
- data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
- data/lib/docyard/components/support/tabs/parser.rb +6 -23
- data/lib/docyard/config/analytics_resolver.rb +24 -0
- data/lib/docyard/config/branding_resolver.rb +58 -27
- data/lib/docyard/config/key_validator.rb +30 -0
- data/lib/docyard/config/logo_detector.rb +8 -8
- data/lib/docyard/config/schema.rb +39 -0
- data/lib/docyard/config/section.rb +21 -0
- data/lib/docyard/config/validation_helpers.rb +83 -0
- data/lib/docyard/config/validator.rb +45 -144
- data/lib/docyard/config/validators/navigation.rb +43 -0
- data/lib/docyard/config/validators/section.rb +114 -0
- data/lib/docyard/config.rb +46 -102
- data/lib/docyard/constants.rb +59 -0
- data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
- data/lib/docyard/initializer.rb +100 -49
- data/lib/docyard/navigation/breadcrumb_builder.rb +45 -6
- data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
- data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
- data/lib/docyard/navigation/sidebar/cache.rb +96 -0
- data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
- data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
- data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
- data/lib/docyard/navigation/sidebar/renderer.rb +12 -1
- data/lib/docyard/navigation/sidebar_builder.rb +43 -81
- data/lib/docyard/rendering/branding_variables.rb +65 -0
- data/lib/docyard/rendering/icon_helpers.rb +14 -1
- data/lib/docyard/rendering/icons/devicons.rb +63 -0
- data/lib/docyard/rendering/icons.rb +26 -27
- data/lib/docyard/rendering/markdown.rb +5 -23
- data/lib/docyard/rendering/og_helpers.rb +36 -0
- data/lib/docyard/rendering/renderer.rb +96 -61
- data/lib/docyard/rendering/template_resolver.rb +14 -0
- data/lib/docyard/routing/fallback_resolver.rb +3 -3
- data/lib/docyard/search/build_indexer.rb +2 -2
- data/lib/docyard/search/dev_indexer.rb +36 -28
- data/lib/docyard/search/pagefind_support.rb +1 -1
- data/lib/docyard/server/asset_handler.rb +39 -15
- data/lib/docyard/server/dev_server.rb +90 -55
- data/lib/docyard/server/file_watcher.rb +68 -18
- data/lib/docyard/server/pagefind_handler.rb +1 -1
- data/lib/docyard/server/preview_server.rb +29 -33
- data/lib/docyard/server/rack_application.rb +39 -71
- data/lib/docyard/server/router.rb +11 -7
- data/lib/docyard/server/sse_server.rb +157 -0
- data/lib/docyard/server/static_file_app.rb +42 -0
- data/lib/docyard/templates/assets/css/components/banner.css +31 -0
- data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
- data/lib/docyard/templates/assets/css/components/callout.css +26 -6
- data/lib/docyard/templates/assets/css/components/code-block.css +4 -2
- data/lib/docyard/templates/assets/css/components/code-group.css +20 -7
- data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
- data/lib/docyard/templates/assets/css/components/file-tree.css +5 -4
- data/lib/docyard/templates/assets/css/components/heading-anchor.css +2 -2
- data/lib/docyard/templates/assets/css/components/icon.css +5 -0
- data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
- data/lib/docyard/templates/assets/css/components/navigation.css +25 -3
- data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
- data/lib/docyard/templates/assets/css/components/prev-next.css +14 -7
- data/lib/docyard/templates/assets/css/components/search.css +6 -10
- data/lib/docyard/templates/assets/css/components/tab-bar.css +9 -6
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +63 -17
- data/lib/docyard/templates/assets/css/components/tabs.css +12 -4
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
- data/lib/docyard/templates/assets/css/landing.css +82 -13
- data/lib/docyard/templates/assets/css/layout.css +32 -16
- data/lib/docyard/templates/assets/css/markdown.css +22 -2
- data/lib/docyard/templates/assets/css/variables.css +14 -1
- data/lib/docyard/templates/assets/js/components/code-group.js +4 -1
- data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
- data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
- data/lib/docyard/templates/assets/js/components/file-tree.js +5 -5
- data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
- data/lib/docyard/templates/assets/js/components/search.js +3 -3
- data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
- data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
- data/lib/docyard/templates/assets/js/components/tooltip.js +4 -4
- data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
- data/lib/docyard/templates/errors/404.html.erb +125 -5
- data/lib/docyard/templates/errors/500.html.erb +184 -10
- data/lib/docyard/templates/errors/redirect.html.erb +12 -0
- data/lib/docyard/templates/init/_sidebar.yml +36 -0
- data/lib/docyard/templates/init/docyard.yml +36 -0
- data/lib/docyard/templates/init/pages/components.md +146 -0
- data/lib/docyard/templates/init/pages/getting-started.md +94 -0
- data/lib/docyard/templates/init/pages/index.md +22 -0
- data/lib/docyard/templates/layouts/default.html.erb +10 -0
- data/lib/docyard/templates/layouts/splash.html.erb +14 -1
- data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
- data/lib/docyard/templates/partials/_banner.html.erb +1 -1
- data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
- data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
- data/lib/docyard/templates/partials/_footer.html.erb +1 -1
- data/lib/docyard/templates/partials/_head.html.erb +80 -5
- data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
- data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
- data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
- data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
- data/lib/docyard/utils/git_info.rb +157 -0
- data/lib/docyard/utils/hash_utils.rb +31 -0
- data/lib/docyard/utils/html_helpers.rb +8 -0
- data/lib/docyard/utils/logging.rb +44 -3
- data/lib/docyard/utils/path_resolver.rb +0 -10
- data/lib/docyard/utils/path_utils.rb +73 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +2 -2
- metadata +81 -47
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- data/.github/pull_request_template.md +0 -14
- data/.github/workflows/ci.yml +0 -49
- data/.rubocop.yml +0 -42
- data/CODE_OF_CONDUCT.md +0 -132
- data/CONTRIBUTING.md +0 -55
- data/LICENSE.vscode-icons +0 -42
- data/Rakefile +0 -8
- data/lib/docyard/config/constants.rb +0 -31
- data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
- data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
- data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -90
- data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
- data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -71
- data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -51
- data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
- data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
- data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
- data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -140
- data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
- data/lib/docyard/rendering/icons/file_types.rb +0 -79
- data/lib/docyard/rendering/icons/phosphor.rb +0 -93
- data/lib/docyard/rendering/language_mapping.rb +0 -52
- data/lib/docyard/templates/assets/js/reload.js +0 -98
- data/lib/docyard/templates/partials/_icon.html.erb +0 -1
- data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
- data/sig/docyard.rbs +0 -4
|
@@ -1,82 +1,142 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
|
-
require_relative "../
|
|
4
|
+
require_relative "../constants"
|
|
5
|
+
require_relative "../utils/git_info"
|
|
5
6
|
require_relative "icon_helpers"
|
|
7
|
+
require_relative "og_helpers"
|
|
8
|
+
require_relative "branding_variables"
|
|
6
9
|
|
|
7
10
|
module Docyard
|
|
8
11
|
class Renderer
|
|
9
12
|
include Utils::UrlHelpers
|
|
13
|
+
include Utils::HtmlHelpers
|
|
10
14
|
include IconHelpers
|
|
15
|
+
include OgHelpers
|
|
16
|
+
include BrandingVariables
|
|
11
17
|
|
|
12
18
|
LAYOUTS_PATH = File.join(__dir__, "../templates", "layouts")
|
|
13
19
|
ERRORS_PATH = File.join(__dir__, "../templates", "errors")
|
|
14
20
|
PARTIALS_PATH = File.join(__dir__, "../templates", "partials")
|
|
15
21
|
DEFAULT_LAYOUT = "default"
|
|
16
22
|
|
|
17
|
-
attr_reader :base_url, :config
|
|
23
|
+
attr_reader :base_url, :config, :dev_mode, :sse_port
|
|
18
24
|
|
|
19
|
-
def initialize(base_url: "/", config: nil)
|
|
25
|
+
def initialize(base_url: "/", config: nil, dev_mode: false, sse_port: nil)
|
|
20
26
|
@base_url = normalize_base_url(base_url)
|
|
21
27
|
@config = config
|
|
28
|
+
@dev_mode = dev_mode
|
|
29
|
+
@sse_port = sse_port
|
|
22
30
|
end
|
|
23
31
|
|
|
24
32
|
def render_file(file_path, sidebar_html: "", prev_next_html: "", breadcrumbs: nil, branding: {},
|
|
25
33
|
template_options: {}, current_path: "/")
|
|
26
|
-
|
|
34
|
+
raw_content = File.read(file_path)
|
|
35
|
+
markdown = Markdown.new(raw_content, config: config, file_path: file_path)
|
|
27
36
|
|
|
28
37
|
render(
|
|
29
38
|
content: strip_md_from_links(markdown.html),
|
|
30
39
|
page_title: markdown.title || Constants::DEFAULT_SITE_TITLE,
|
|
40
|
+
page_description: markdown.description,
|
|
41
|
+
page_og_image: markdown.og_image,
|
|
31
42
|
navigation: build_navigation(sidebar_html, prev_next_html, markdown.toc, breadcrumbs),
|
|
32
43
|
branding: branding,
|
|
33
44
|
template_options: template_options,
|
|
34
|
-
current_path: current_path
|
|
45
|
+
current_path: current_path,
|
|
46
|
+
file_path: file_path,
|
|
47
|
+
raw_markdown: raw_content
|
|
35
48
|
)
|
|
36
49
|
end
|
|
37
50
|
|
|
51
|
+
def render_for_search(file_path)
|
|
52
|
+
markdown = Markdown.new(File.read(file_path), config: config, file_path: file_path)
|
|
53
|
+
title = markdown.title || Constants::DEFAULT_SITE_TITLE
|
|
54
|
+
content = strip_md_from_links(markdown.html)
|
|
55
|
+
|
|
56
|
+
<<~HTML
|
|
57
|
+
<!DOCTYPE html>
|
|
58
|
+
<html>
|
|
59
|
+
<head><title>#{escape_html(title)}</title></head>
|
|
60
|
+
<body><main data-pagefind-body>#{content}</main></body>
|
|
61
|
+
</html>
|
|
62
|
+
HTML
|
|
63
|
+
end
|
|
64
|
+
|
|
38
65
|
def build_navigation(sidebar_html, prev_next_html, toc, breadcrumbs)
|
|
39
66
|
{ sidebar_html: sidebar_html, prev_next_html: prev_next_html, toc: toc, breadcrumbs: breadcrumbs }
|
|
40
67
|
end
|
|
41
68
|
|
|
42
|
-
def render(content:, page_title: Constants::DEFAULT_SITE_TITLE,
|
|
43
|
-
template_options: {}, current_path: "/"
|
|
69
|
+
def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, page_description: nil, page_og_image: nil,
|
|
70
|
+
navigation: {}, branding: {}, template_options: {}, current_path: "/", file_path: nil,
|
|
71
|
+
raw_markdown: nil)
|
|
44
72
|
layout = template_options[:template] || DEFAULT_LAYOUT
|
|
45
73
|
layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
|
|
46
74
|
template = File.read(layout_path)
|
|
47
75
|
|
|
48
|
-
assign_content_variables(content, page_title, navigation)
|
|
76
|
+
assign_content_variables(content, page_title, navigation, raw_markdown)
|
|
49
77
|
assign_branding_variables(branding, current_path)
|
|
78
|
+
assign_og_variables(branding, page_description, page_og_image, current_path)
|
|
50
79
|
assign_template_variables(template_options)
|
|
80
|
+
assign_git_info(branding, file_path)
|
|
81
|
+
assign_feedback_variables
|
|
51
82
|
|
|
52
83
|
ERB.new(template).result(binding)
|
|
53
84
|
end
|
|
54
85
|
|
|
55
|
-
def render_not_found
|
|
86
|
+
def render_not_found(branding: nil)
|
|
87
|
+
@primary_color = branding&.dig(:primary_color)
|
|
56
88
|
render_error_template(404)
|
|
57
89
|
end
|
|
58
90
|
|
|
59
|
-
def render_server_error(error)
|
|
91
|
+
def render_server_error(error, branding: nil)
|
|
60
92
|
@error_message = error.message
|
|
61
|
-
@backtrace = error.backtrace
|
|
93
|
+
@backtrace = error.backtrace&.join("\n") || "No backtrace available"
|
|
94
|
+
@primary_color = branding&.dig(:primary_color)
|
|
62
95
|
render_error_template(500)
|
|
63
96
|
end
|
|
64
97
|
|
|
98
|
+
def render_redirect(target_url)
|
|
99
|
+
@target_url = target_url
|
|
100
|
+
render_error_template(:redirect)
|
|
101
|
+
end
|
|
102
|
+
|
|
65
103
|
def render_error_template(status)
|
|
66
104
|
error_template_path = File.join(ERRORS_PATH, "#{status}.html.erb")
|
|
67
105
|
template = File.read(error_template_path)
|
|
68
106
|
ERB.new(template).result(binding)
|
|
69
107
|
end
|
|
70
108
|
|
|
109
|
+
VALID_IVAR_PATTERN = /\A[a-z_][a-z0-9_]*\z/i
|
|
110
|
+
|
|
71
111
|
def render_partial(name, locals = {})
|
|
72
112
|
partial_path = File.join(PARTIALS_PATH, "#{name}.html.erb")
|
|
73
113
|
template = File.read(partial_path)
|
|
74
114
|
|
|
75
|
-
locals.each
|
|
115
|
+
locals.each do |key, value|
|
|
116
|
+
validate_variable_name!(key)
|
|
117
|
+
instance_variable_set("@#{key}", value)
|
|
118
|
+
end
|
|
76
119
|
|
|
77
120
|
ERB.new(template).result(binding)
|
|
78
121
|
end
|
|
79
122
|
|
|
123
|
+
def render_custom_visual(file_path)
|
|
124
|
+
return "" if file_path.nil? || file_path.empty?
|
|
125
|
+
|
|
126
|
+
source_dir = config&.source || "docs"
|
|
127
|
+
full_path = File.join(source_dir, file_path)
|
|
128
|
+
|
|
129
|
+
return "" unless File.exist?(full_path)
|
|
130
|
+
|
|
131
|
+
File.read(full_path)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validate_variable_name!(name)
|
|
135
|
+
return if name.to_s.match?(VALID_IVAR_PATTERN)
|
|
136
|
+
|
|
137
|
+
raise ArgumentError, "Invalid variable name: #{name}"
|
|
138
|
+
end
|
|
139
|
+
|
|
80
140
|
def asset_path(path)
|
|
81
141
|
return path if path.nil? || path.start_with?("http://", "https://")
|
|
82
142
|
|
|
@@ -85,61 +145,14 @@ module Docyard
|
|
|
85
145
|
|
|
86
146
|
private
|
|
87
147
|
|
|
88
|
-
def assign_content_variables(content, page_title, navigation)
|
|
148
|
+
def assign_content_variables(content, page_title, navigation, raw_markdown)
|
|
89
149
|
@content = content
|
|
90
150
|
@page_title = page_title
|
|
91
151
|
@sidebar_html = navigation[:sidebar_html] || ""
|
|
92
152
|
@prev_next_html = navigation[:prev_next_html] || ""
|
|
93
153
|
@toc = navigation[:toc] || []
|
|
94
154
|
@breadcrumbs = navigation[:breadcrumbs]
|
|
95
|
-
|
|
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
|
-
@announcement = branding[:announcement]
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def assign_tabs(branding, current_path)
|
|
127
|
-
tabs = branding[:tabs] || []
|
|
128
|
-
@tabs = tabs.map { |tab| tab.merge(active: tab_active?(tab[:href], current_path)) }
|
|
129
|
-
@has_tabs = branding[:has_tabs] || false
|
|
130
|
-
@current_path = current_path
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def tab_active?(tab_href, current_path)
|
|
134
|
-
return false if tab_href.nil? || current_path.nil?
|
|
135
|
-
return false if tab_href.start_with?("http://", "https://")
|
|
136
|
-
|
|
137
|
-
normalized_tab = tab_href.chomp("/")
|
|
138
|
-
normalized_current = current_path.chomp("/")
|
|
139
|
-
|
|
140
|
-
return true if normalized_tab == normalized_current
|
|
141
|
-
|
|
142
|
-
current_path.start_with?("#{normalized_tab}/")
|
|
155
|
+
@raw_markdown = raw_markdown
|
|
143
156
|
end
|
|
144
157
|
|
|
145
158
|
def assign_template_variables(template_options)
|
|
@@ -160,5 +173,27 @@ module Docyard
|
|
|
160
173
|
def strip_md_from_links(html)
|
|
161
174
|
html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
|
|
162
175
|
end
|
|
176
|
+
|
|
177
|
+
def assign_git_info(branding, file_path)
|
|
178
|
+
@show_edit_link = branding[:show_edit_link] && file_path
|
|
179
|
+
@show_last_updated = branding[:show_last_updated] && file_path
|
|
180
|
+
return unless @show_edit_link || @show_last_updated
|
|
181
|
+
|
|
182
|
+
git_info = Utils::GitInfo.new(
|
|
183
|
+
repo_url: branding[:repo_url],
|
|
184
|
+
branch: branding[:repo_branch],
|
|
185
|
+
edit_path: branding[:repo_edit_path]
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@edit_url = git_info.edit_url(file_path) if @show_edit_link
|
|
189
|
+
@last_updated = git_info.last_updated(file_path) if @show_last_updated
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def assign_feedback_variables
|
|
193
|
+
return unless config
|
|
194
|
+
|
|
195
|
+
@feedback_enabled = config.feedback.enabled
|
|
196
|
+
@feedback_question = config.feedback.question
|
|
197
|
+
end
|
|
163
198
|
end
|
|
164
199
|
end
|
|
@@ -117,6 +117,7 @@ module Docyard
|
|
|
117
117
|
tagline: hero["tagline"],
|
|
118
118
|
gradient: hero.fetch("gradient", true),
|
|
119
119
|
image: symbolize_image(hero["image"]),
|
|
120
|
+
custom_visual: symbolize_custom_visual(hero["custom_visual"]),
|
|
120
121
|
actions: symbolize_actions(hero["actions"])
|
|
121
122
|
}.compact
|
|
122
123
|
end
|
|
@@ -138,6 +139,19 @@ module Docyard
|
|
|
138
139
|
end
|
|
139
140
|
end
|
|
140
141
|
|
|
142
|
+
def symbolize_custom_visual(custom_visual)
|
|
143
|
+
return nil if custom_visual.nil?
|
|
144
|
+
|
|
145
|
+
if custom_visual.is_a?(String)
|
|
146
|
+
{ html: custom_visual, placement: "side" }
|
|
147
|
+
elsif custom_visual.is_a?(Hash)
|
|
148
|
+
{
|
|
149
|
+
html: custom_visual["html"],
|
|
150
|
+
placement: custom_visual["placement"] || "side"
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
141
155
|
def symbolize_actions(actions)
|
|
142
156
|
return nil unless actions.is_a?(Array)
|
|
143
157
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../utils/path_utils"
|
|
4
|
+
|
|
3
5
|
module Docyard
|
|
4
6
|
module Routing
|
|
5
7
|
class FallbackResolver
|
|
@@ -29,9 +31,7 @@ module Docyard
|
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
def sanitize_path(request_path)
|
|
32
|
-
|
|
33
|
-
clean = "index" if clean.empty?
|
|
34
|
-
clean.delete_suffix(".md")
|
|
34
|
+
Utils::PathUtils.sanitize_url_path(request_path)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def find_first_item_in_section(request_path)
|
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "open3"
|
|
6
|
+
require "parallel"
|
|
6
7
|
require "tty-progressbar"
|
|
8
|
+
require_relative "../utils/path_utils"
|
|
7
9
|
|
|
8
10
|
module Docyard
|
|
9
11
|
module Search
|
|
10
12
|
class DevIndexer
|
|
11
13
|
include PagefindSupport
|
|
12
14
|
|
|
15
|
+
PARALLEL_THRESHOLD = 10
|
|
16
|
+
|
|
13
17
|
attr_reader :docs_path, :config, :temp_dir, :pagefind_path
|
|
14
18
|
|
|
15
19
|
def initialize(docs_path:, config:)
|
|
@@ -26,12 +30,12 @@ module Docyard
|
|
|
26
30
|
@temp_dir = Dir.mktmpdir("docyard-search-")
|
|
27
31
|
generate_html_files
|
|
28
32
|
page_count = run_pagefind
|
|
29
|
-
@pagefind_path = File.join(temp_dir, "pagefind")
|
|
33
|
+
@pagefind_path = File.join(temp_dir, "_docyard", "pagefind")
|
|
30
34
|
|
|
31
35
|
log_success(page_count)
|
|
32
36
|
pagefind_path
|
|
33
37
|
rescue StandardError => e
|
|
34
|
-
warn
|
|
38
|
+
Docyard.logger.warn("Search index generation failed: #{e.message}")
|
|
35
39
|
cleanup
|
|
36
40
|
nil
|
|
37
41
|
end
|
|
@@ -46,7 +50,7 @@ module Docyard
|
|
|
46
50
|
|
|
47
51
|
def pagefind_available?
|
|
48
52
|
result = super
|
|
49
|
-
warn
|
|
53
|
+
Docyard.logger.warn("Search disabled: Pagefind not found (npm install -g pagefind)") unless result
|
|
50
54
|
result
|
|
51
55
|
end
|
|
52
56
|
|
|
@@ -54,20 +58,43 @@ module Docyard
|
|
|
54
58
|
markdown_files = Dir.glob(File.join(docs_path, "**", "*.md"))
|
|
55
59
|
markdown_files = filter_excluded_files(markdown_files)
|
|
56
60
|
markdown_files = filter_non_indexable_files(markdown_files)
|
|
57
|
-
renderer = Renderer.new(base_url: "/", config: config)
|
|
58
61
|
|
|
59
62
|
progress = TTY::ProgressBar.new(
|
|
60
63
|
"Indexing search [:bar] :current/:total (:percent)",
|
|
61
64
|
total: markdown_files.size,
|
|
62
65
|
width: 50
|
|
63
66
|
)
|
|
67
|
+
mutex = Mutex.new
|
|
68
|
+
|
|
69
|
+
Logging.start_buffering
|
|
70
|
+
if markdown_files.size >= PARALLEL_THRESHOLD
|
|
71
|
+
generate_files_in_parallel(markdown_files, progress, mutex)
|
|
72
|
+
else
|
|
73
|
+
generate_files_sequentially(markdown_files, progress)
|
|
74
|
+
end
|
|
75
|
+
Logging.flush_warnings
|
|
76
|
+
end
|
|
64
77
|
|
|
78
|
+
def generate_files_in_parallel(markdown_files, progress, mutex)
|
|
79
|
+
Parallel.each(markdown_files, in_threads: Parallel.processor_count) do |file_path|
|
|
80
|
+
renderer = thread_local_renderer
|
|
81
|
+
generate_html_file(file_path, renderer)
|
|
82
|
+
mutex.synchronize { progress.advance }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def generate_files_sequentially(markdown_files, progress)
|
|
87
|
+
renderer = Renderer.new(base_url: "/", config: config)
|
|
65
88
|
markdown_files.each do |file_path|
|
|
66
89
|
generate_html_file(file_path, renderer)
|
|
67
90
|
progress.advance
|
|
68
91
|
end
|
|
69
92
|
end
|
|
70
93
|
|
|
94
|
+
def thread_local_renderer
|
|
95
|
+
Thread.current[:docyard_search_renderer] ||= Renderer.new(base_url: "/", config: config)
|
|
96
|
+
end
|
|
97
|
+
|
|
71
98
|
def filter_excluded_files(files)
|
|
72
99
|
exclude_patterns = config.search.exclude || []
|
|
73
100
|
return files if exclude_patterns.empty?
|
|
@@ -96,40 +123,21 @@ module Docyard
|
|
|
96
123
|
end
|
|
97
124
|
|
|
98
125
|
def file_to_url_path(file_path)
|
|
99
|
-
|
|
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
|
|
126
|
+
Utils::PathUtils.markdown_file_to_url(file_path, docs_path)
|
|
108
127
|
end
|
|
109
128
|
|
|
110
129
|
def generate_html_file(markdown_file, renderer)
|
|
111
130
|
relative_path = markdown_file.delete_prefix("#{docs_path}/")
|
|
112
131
|
output_path = determine_output_path(relative_path)
|
|
113
132
|
|
|
114
|
-
html = renderer.
|
|
133
|
+
html = renderer.render_for_search(markdown_file)
|
|
115
134
|
|
|
116
135
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
117
136
|
File.write(output_path, html)
|
|
118
137
|
end
|
|
119
138
|
|
|
120
139
|
def determine_output_path(relative_path)
|
|
121
|
-
|
|
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
|
|
140
|
+
Utils::PathUtils.markdown_to_html_output(relative_path, temp_dir)
|
|
133
141
|
end
|
|
134
142
|
|
|
135
143
|
def run_pagefind
|
|
@@ -147,8 +155,8 @@ module Docyard
|
|
|
147
155
|
end
|
|
148
156
|
|
|
149
157
|
def log_success(page_count)
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
Docyard.logger.info("* Search index generated (#{page_count} pages indexed)")
|
|
159
|
+
Docyard.logger.debug("* Temp directory: #{temp_dir}")
|
|
152
160
|
end
|
|
153
161
|
end
|
|
154
162
|
end
|
|
@@ -17,7 +17,7 @@ module Docyard
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def build_pagefind_args(site_dir)
|
|
20
|
-
args = ["pagefind", "--site", site_dir]
|
|
20
|
+
args = ["pagefind", "--site", site_dir, "--output-subdir", "_docyard/pagefind"]
|
|
21
21
|
|
|
22
22
|
exclusions = config.search.exclude || []
|
|
23
23
|
exclusions.each do |pattern|
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
require_relative "../utils/path_utils"
|
|
5
|
+
|
|
3
6
|
module Docyard
|
|
4
7
|
class AssetHandler
|
|
5
8
|
TEMPLATES_ASSETS_PATH = File.join(__dir__, "../templates", "assets")
|
|
9
|
+
CACHE_MAX_AGE = 3600
|
|
10
|
+
DEFAULT_PUBLIC_DIR = "docs/public"
|
|
11
|
+
|
|
12
|
+
attr_reader :public_dir
|
|
6
13
|
|
|
7
14
|
CONTENT_TYPES = {
|
|
8
15
|
".css" => "text/css; charset=utf-8",
|
|
@@ -23,47 +30,52 @@ module Docyard
|
|
|
23
30
|
".webm" => "video/webm"
|
|
24
31
|
}.freeze
|
|
25
32
|
|
|
26
|
-
def
|
|
27
|
-
|
|
33
|
+
def initialize(public_dir: DEFAULT_PUBLIC_DIR)
|
|
34
|
+
@public_dir = public_dir
|
|
35
|
+
end
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
def serve_docyard_assets(request_path)
|
|
38
|
+
asset_path = Utils::PathUtils.decode_path(request_path.delete_prefix("/_docyard/"))
|
|
30
39
|
|
|
31
40
|
return serve_components_css if asset_path == "css/components.css"
|
|
32
41
|
return serve_components_js if asset_path == "js/components.js"
|
|
33
42
|
|
|
34
|
-
file_path =
|
|
43
|
+
file_path = safe_asset_path(asset_path, TEMPLATES_ASSETS_PATH)
|
|
44
|
+
return forbidden_response unless file_path
|
|
35
45
|
return not_found_response unless File.file?(file_path)
|
|
36
46
|
|
|
37
47
|
serve_file(file_path)
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
def serve_public_file(request_path)
|
|
41
|
-
asset_path = request_path.delete_prefix("/")
|
|
51
|
+
asset_path = Utils::PathUtils.decode_path(request_path.delete_prefix("/"))
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
file_path = File.join(Constants::PUBLIC_DIR, asset_path)
|
|
46
|
-
return nil unless File.file?(file_path)
|
|
53
|
+
file_path = safe_asset_path(asset_path, public_dir)
|
|
54
|
+
return nil unless file_path && File.file?(file_path)
|
|
47
55
|
|
|
48
56
|
serve_file(file_path)
|
|
49
57
|
end
|
|
50
58
|
|
|
51
59
|
private
|
|
52
60
|
|
|
53
|
-
def
|
|
54
|
-
|
|
61
|
+
def safe_asset_path(relative_path, base_dir)
|
|
62
|
+
Utils::PathUtils.resolve_safe_path(relative_path, base_dir)
|
|
55
63
|
end
|
|
56
64
|
|
|
57
65
|
def serve_file(file_path)
|
|
58
66
|
content = File.read(file_path)
|
|
59
|
-
|
|
67
|
+
headers = build_cache_headers(content, File.mtime(file_path))
|
|
68
|
+
headers["Content-Type"] = detect_content_type(file_path)
|
|
60
69
|
|
|
61
|
-
[200,
|
|
70
|
+
[200, headers, [content]]
|
|
62
71
|
end
|
|
63
72
|
|
|
64
73
|
def serve_components_css
|
|
65
74
|
content = concatenate_component_css
|
|
66
|
-
|
|
75
|
+
headers = build_cache_headers(content)
|
|
76
|
+
headers["Content-Type"] = "text/css; charset=utf-8"
|
|
77
|
+
|
|
78
|
+
[200, headers, [content]]
|
|
67
79
|
end
|
|
68
80
|
|
|
69
81
|
def concatenate_component_css
|
|
@@ -76,7 +88,10 @@ module Docyard
|
|
|
76
88
|
|
|
77
89
|
def serve_components_js
|
|
78
90
|
content = concatenate_component_js
|
|
79
|
-
|
|
91
|
+
headers = build_cache_headers(content)
|
|
92
|
+
headers["Content-Type"] = "application/javascript; charset=utf-8"
|
|
93
|
+
|
|
94
|
+
[200, headers, [content]]
|
|
80
95
|
end
|
|
81
96
|
|
|
82
97
|
def concatenate_component_js
|
|
@@ -92,6 +107,15 @@ module Docyard
|
|
|
92
107
|
CONTENT_TYPES.fetch(extension, "application/octet-stream")
|
|
93
108
|
end
|
|
94
109
|
|
|
110
|
+
def build_cache_headers(content, last_modified = nil)
|
|
111
|
+
headers = {
|
|
112
|
+
"Cache-Control" => "public, max-age=#{CACHE_MAX_AGE}",
|
|
113
|
+
"ETag" => %("#{Digest::MD5.hexdigest(content)}")
|
|
114
|
+
}
|
|
115
|
+
headers["Last-Modified"] = last_modified.httpdate if last_modified
|
|
116
|
+
headers
|
|
117
|
+
end
|
|
118
|
+
|
|
95
119
|
def forbidden_response
|
|
96
120
|
[403, { "Content-Type" => "text/plain" }, ["403 Forbidden"]]
|
|
97
121
|
end
|