docyard 0.8.0 → 1.0.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 +67 -1
- data/README.md +8 -253
- data/exe/docyard +6 -0
- data/lib/docyard/build/asset_bundler.rb +2 -2
- data/lib/docyard/build/file_copier.rb +12 -5
- data/lib/docyard/build/llms_txt_generator.rb +103 -0
- data/lib/docyard/build/sitemap_generator.rb +1 -1
- data/lib/docyard/build/static_generator.rb +115 -79
- data/lib/docyard/builder.rb +6 -2
- data/lib/docyard/cli.rb +14 -4
- 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 +9 -3
- data/lib/docyard/components/processors/cards_processor.rb +100 -0
- 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 +34 -3
- data/lib/docyard/components/processors/code_block_processor.rb +11 -24
- data/lib/docyard/components/processors/code_group_processor.rb +182 -0
- data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +7 -1
- data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
- data/lib/docyard/components/processors/file_tree_processor.rb +150 -0
- data/lib/docyard/components/processors/icon_processor.rb +8 -2
- 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 +207 -0
- 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 +118 -0
- data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
- 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 +84 -58
- data/lib/docyard/config/key_validator.rb +30 -0
- data/lib/docyard/config/logo_detector.rb +39 -0
- 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 +45 -96
- 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/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/item.rb +6 -1
- data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
- data/lib/docyard/navigation/sidebar/renderer.rb +18 -3
- 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 +20 -15
- data/lib/docyard/rendering/og_helpers.rb +36 -0
- data/lib/docyard/rendering/renderer.rb +87 -58
- 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 +40 -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 +38 -70
- 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/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 +233 -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/cards.css +100 -0
- data/lib/docyard/templates/assets/css/components/code-block.css +14 -2
- data/lib/docyard/templates/assets/css/components/code-group.css +294 -0
- data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
- data/lib/docyard/templates/assets/css/components/figure.css +22 -0
- data/lib/docyard/templates/assets/css/components/file-tree.css +125 -0
- data/lib/docyard/templates/assets/css/components/heading-anchor.css +21 -13
- data/lib/docyard/templates/assets/css/components/icon.css +5 -0
- data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
- data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
- data/lib/docyard/templates/assets/css/components/navigation.css +32 -3
- data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
- data/lib/docyard/templates/assets/css/components/prev-next.css +20 -22
- data/lib/docyard/templates/assets/css/components/search.css +6 -10
- data/lib/docyard/templates/assets/css/components/steps.css +122 -0
- data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
- data/lib/docyard/templates/assets/css/components/tabs.css +13 -5
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -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/landing.css +82 -13
- data/lib/docyard/templates/assets/css/layout.css +17 -0
- data/lib/docyard/templates/assets/css/markdown.css +25 -3
- data/lib/docyard/templates/assets/css/variables.css +13 -1
- 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 +286 -0
- 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 +39 -0
- data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
- 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 +118 -0
- data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
- data/lib/docyard/templates/errors/404.html.erb +114 -5
- data/lib/docyard/templates/errors/500.html.erb +173 -10
- 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 +11 -0
- data/lib/docyard/templates/layouts/splash.html.erb +15 -1
- data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
- data/lib/docyard/templates/partials/_analytics.html.erb +24 -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/_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 +79 -4
- data/lib/docyard/templates/partials/_icon_library.html.erb +8 -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/_page_actions.html.erb +21 -0
- data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
- data/lib/docyard/templates/partials/_step.html.erb +14 -0
- 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 +114 -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 -78
- data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
- data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -69
- data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -47
- 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 -139
- 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 -90
- 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,53 +1,84 @@
|
|
|
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
|
|
@@ -58,7 +89,7 @@ module Docyard
|
|
|
58
89
|
|
|
59
90
|
def render_server_error(error)
|
|
60
91
|
@error_message = error.message
|
|
61
|
-
@backtrace = error.backtrace
|
|
92
|
+
@backtrace = error.backtrace&.join("\n") || "No backtrace available"
|
|
62
93
|
render_error_template(500)
|
|
63
94
|
end
|
|
64
95
|
|
|
@@ -68,15 +99,37 @@ module Docyard
|
|
|
68
99
|
ERB.new(template).result(binding)
|
|
69
100
|
end
|
|
70
101
|
|
|
102
|
+
VALID_IVAR_PATTERN = /\A[a-z_][a-z0-9_]*\z/i
|
|
103
|
+
|
|
71
104
|
def render_partial(name, locals = {})
|
|
72
105
|
partial_path = File.join(PARTIALS_PATH, "#{name}.html.erb")
|
|
73
106
|
template = File.read(partial_path)
|
|
74
107
|
|
|
75
|
-
locals.each
|
|
108
|
+
locals.each do |key, value|
|
|
109
|
+
validate_variable_name!(key)
|
|
110
|
+
instance_variable_set("@#{key}", value)
|
|
111
|
+
end
|
|
76
112
|
|
|
77
113
|
ERB.new(template).result(binding)
|
|
78
114
|
end
|
|
79
115
|
|
|
116
|
+
def render_custom_visual(file_path)
|
|
117
|
+
return "" if file_path.nil? || file_path.empty?
|
|
118
|
+
|
|
119
|
+
source_dir = config&.source || "docs"
|
|
120
|
+
full_path = File.join(source_dir, file_path)
|
|
121
|
+
|
|
122
|
+
return "" unless File.exist?(full_path)
|
|
123
|
+
|
|
124
|
+
File.read(full_path)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validate_variable_name!(name)
|
|
128
|
+
return if name.to_s.match?(VALID_IVAR_PATTERN)
|
|
129
|
+
|
|
130
|
+
raise ArgumentError, "Invalid variable name: #{name}"
|
|
131
|
+
end
|
|
132
|
+
|
|
80
133
|
def asset_path(path)
|
|
81
134
|
return path if path.nil? || path.start_with?("http://", "https://")
|
|
82
135
|
|
|
@@ -85,60 +138,14 @@ module Docyard
|
|
|
85
138
|
|
|
86
139
|
private
|
|
87
140
|
|
|
88
|
-
def assign_content_variables(content, page_title, navigation)
|
|
141
|
+
def assign_content_variables(content, page_title, navigation, raw_markdown)
|
|
89
142
|
@content = content
|
|
90
143
|
@page_title = page_title
|
|
91
144
|
@sidebar_html = navigation[:sidebar_html] || ""
|
|
92
145
|
@prev_next_html = navigation[:prev_next_html] || ""
|
|
93
146
|
@toc = navigation[:toc] || []
|
|
94
147
|
@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
|
-
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}/")
|
|
148
|
+
@raw_markdown = raw_markdown
|
|
142
149
|
end
|
|
143
150
|
|
|
144
151
|
def assign_template_variables(template_options)
|
|
@@ -159,5 +166,27 @@ module Docyard
|
|
|
159
166
|
def strip_md_from_links(html)
|
|
160
167
|
html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
|
|
161
168
|
end
|
|
169
|
+
|
|
170
|
+
def assign_git_info(branding, file_path)
|
|
171
|
+
@show_edit_link = branding[:show_edit_link] && file_path
|
|
172
|
+
@show_last_updated = branding[:show_last_updated] && file_path
|
|
173
|
+
return unless @show_edit_link || @show_last_updated
|
|
174
|
+
|
|
175
|
+
git_info = Utils::GitInfo.new(
|
|
176
|
+
repo_url: branding[:repo_url],
|
|
177
|
+
branch: branding[:repo_branch],
|
|
178
|
+
edit_path: branding[:repo_edit_path]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
@edit_url = git_info.edit_url(file_path) if @show_edit_link
|
|
182
|
+
@last_updated = git_info.last_updated(file_path) if @show_last_updated
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def assign_feedback_variables
|
|
186
|
+
return unless config
|
|
187
|
+
|
|
188
|
+
@feedback_enabled = config.feedback.enabled
|
|
189
|
+
@feedback_question = config.feedback.question
|
|
190
|
+
end
|
|
162
191
|
end
|
|
163
192
|
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",
|
|
@@ -12,6 +19,7 @@ module Docyard
|
|
|
12
19
|
".jpeg" => "image/jpeg",
|
|
13
20
|
".gif" => "image/gif",
|
|
14
21
|
".webp" => "image/webp",
|
|
22
|
+
".avif" => "image/avif",
|
|
15
23
|
".svg" => "image/svg+xml",
|
|
16
24
|
".woff" => "font/woff",
|
|
17
25
|
".woff2" => "font/woff2",
|
|
@@ -22,47 +30,52 @@ module Docyard
|
|
|
22
30
|
".webm" => "video/webm"
|
|
23
31
|
}.freeze
|
|
24
32
|
|
|
25
|
-
def
|
|
26
|
-
|
|
33
|
+
def initialize(public_dir: DEFAULT_PUBLIC_DIR)
|
|
34
|
+
@public_dir = public_dir
|
|
35
|
+
end
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
def serve_docyard_assets(request_path)
|
|
38
|
+
asset_path = Utils::PathUtils.decode_path(request_path.delete_prefix("/_docyard/"))
|
|
29
39
|
|
|
30
40
|
return serve_components_css if asset_path == "css/components.css"
|
|
31
41
|
return serve_components_js if asset_path == "js/components.js"
|
|
32
42
|
|
|
33
|
-
file_path =
|
|
43
|
+
file_path = safe_asset_path(asset_path, TEMPLATES_ASSETS_PATH)
|
|
44
|
+
return forbidden_response unless file_path
|
|
34
45
|
return not_found_response unless File.file?(file_path)
|
|
35
46
|
|
|
36
47
|
serve_file(file_path)
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
def serve_public_file(request_path)
|
|
40
|
-
asset_path = request_path.delete_prefix("/")
|
|
51
|
+
asset_path = Utils::PathUtils.decode_path(request_path.delete_prefix("/"))
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
file_path = File.join(Constants::PUBLIC_DIR, asset_path)
|
|
45
|
-
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)
|
|
46
55
|
|
|
47
56
|
serve_file(file_path)
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
private
|
|
51
60
|
|
|
52
|
-
def
|
|
53
|
-
|
|
61
|
+
def safe_asset_path(relative_path, base_dir)
|
|
62
|
+
Utils::PathUtils.resolve_safe_path(relative_path, base_dir)
|
|
54
63
|
end
|
|
55
64
|
|
|
56
65
|
def serve_file(file_path)
|
|
57
66
|
content = File.read(file_path)
|
|
58
|
-
|
|
67
|
+
headers = build_cache_headers(content, File.mtime(file_path))
|
|
68
|
+
headers["Content-Type"] = detect_content_type(file_path)
|
|
59
69
|
|
|
60
|
-
[200,
|
|
70
|
+
[200, headers, [content]]
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
def serve_components_css
|
|
64
74
|
content = concatenate_component_css
|
|
65
|
-
|
|
75
|
+
headers = build_cache_headers(content)
|
|
76
|
+
headers["Content-Type"] = "text/css; charset=utf-8"
|
|
77
|
+
|
|
78
|
+
[200, headers, [content]]
|
|
66
79
|
end
|
|
67
80
|
|
|
68
81
|
def concatenate_component_css
|
|
@@ -75,7 +88,10 @@ module Docyard
|
|
|
75
88
|
|
|
76
89
|
def serve_components_js
|
|
77
90
|
content = concatenate_component_js
|
|
78
|
-
|
|
91
|
+
headers = build_cache_headers(content)
|
|
92
|
+
headers["Content-Type"] = "application/javascript; charset=utf-8"
|
|
93
|
+
|
|
94
|
+
[200, headers, [content]]
|
|
79
95
|
end
|
|
80
96
|
|
|
81
97
|
def concatenate_component_js
|
|
@@ -91,6 +107,15 @@ module Docyard
|
|
|
91
107
|
CONTENT_TYPES.fetch(extension, "application/octet-stream")
|
|
92
108
|
end
|
|
93
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
|
+
|
|
94
119
|
def forbidden_response
|
|
95
120
|
[403, { "Content-Type" => "text/plain" }, ["403 Forbidden"]]
|
|
96
121
|
end
|