docyard 0.6.0 → 0.7.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 +15 -1
- data/lib/docyard/build/static_generator.rb +2 -43
- data/lib/docyard/builder.rb +14 -4
- 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 +74 -0
- data/lib/docyard/{constants.rb → config/constants.rb} +1 -0
- data/lib/docyard/config.rb +10 -1
- data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +2 -2
- data/lib/docyard/{sidebar → navigation/sidebar}/renderer.rb +3 -14
- data/lib/docyard/{sidebar → navigation/sidebar}/tree_builder.rb +9 -2
- data/lib/docyard/{sidebar_builder.rb → navigation/sidebar_builder.rb} +3 -15
- data/lib/docyard/{icons → rendering/icons}/phosphor.rb +4 -1
- data/lib/docyard/{markdown.rb → rendering/markdown.rb} +14 -13
- data/lib/docyard/{renderer.rb → rendering/renderer.rb} +20 -17
- data/lib/docyard/search/build_indexer.rb +74 -0
- data/lib/docyard/search/dev_indexer.rb +110 -0
- data/lib/docyard/search/pagefind_support.rb +31 -0
- data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +1 -1
- data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
- data/lib/docyard/{preview_server.rb → server/preview_server.rb} +1 -1
- data/lib/docyard/{rack_application.rb → server/rack_application.rb} +52 -49
- 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/components/search.css +549 -0
- data/lib/docyard/templates/assets/css/layout.css +15 -1
- data/lib/docyard/templates/assets/js/components/search.js +685 -0
- data/lib/docyard/templates/layouts/default.html.erb +14 -2
- data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
- data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -1
- data/lib/docyard/templates/partials/_prev_next.html.erb +1 -1
- data/lib/docyard/templates/partials/_search_modal.html.erb +45 -0
- data/lib/docyard/templates/partials/_search_trigger.html.erb +22 -0
- 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 +57 -46
- 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/routing/resolution_result.rb +0 -31
- /data/lib/docyard/{sidebar → navigation/sidebar}/config_parser.rb +0 -0
- /data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +0 -0
- /data/lib/docyard/{sidebar → navigation/sidebar}/item.rb +0 -0
- /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
|
@@ -5,7 +5,9 @@ require "erb"
|
|
|
5
5
|
module Docyard
|
|
6
6
|
module Sidebar
|
|
7
7
|
class Renderer
|
|
8
|
-
|
|
8
|
+
include Utils::UrlHelpers
|
|
9
|
+
|
|
10
|
+
PARTIALS_PATH = File.join(__dir__, "../../templates/partials")
|
|
9
11
|
|
|
10
12
|
attr_reader :site_title, :base_url
|
|
11
13
|
|
|
@@ -39,19 +41,6 @@ module Docyard
|
|
|
39
41
|
Icons.render(name.to_s.tr("_", "-"), weight) || ""
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
def link_path(path)
|
|
43
|
-
return path if path.nil? || path.start_with?("http://", "https://")
|
|
44
|
-
|
|
45
|
-
"#{base_url.chomp('/')}#{path}"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def normalize_base_url(url)
|
|
49
|
-
return "/" if url.nil? || url.empty?
|
|
50
|
-
|
|
51
|
-
url = "/#{url}" unless url.start_with?("/")
|
|
52
|
-
url.end_with?("/") ? url : "#{url}/"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
44
|
def render_tree_with_sections(items)
|
|
56
45
|
filtered_items = items.reject { |item| item[:title]&.downcase == site_title.downcase }
|
|
57
46
|
grouped_items = group_by_section(filtered_items)
|
|
@@ -29,6 +29,7 @@ module Docyard
|
|
|
29
29
|
|
|
30
30
|
def transform_directory(item, relative_base)
|
|
31
31
|
dir_path = File.join(relative_base, item[:name])
|
|
32
|
+
children = transform_items(item[:children], dir_path)
|
|
32
33
|
|
|
33
34
|
{
|
|
34
35
|
title: Utils::TextFormatter.titleize(item[:name]),
|
|
@@ -36,11 +37,17 @@ module Docyard
|
|
|
36
37
|
active: false,
|
|
37
38
|
type: :directory,
|
|
38
39
|
collapsible: true,
|
|
39
|
-
collapsed:
|
|
40
|
-
children:
|
|
40
|
+
collapsed: !active_child?(children),
|
|
41
|
+
children: children
|
|
41
42
|
}
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
def active_child?(children)
|
|
46
|
+
children.any? do |child|
|
|
47
|
+
child[:active] || active_child?(child[:children] || [])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
44
51
|
def transform_file(item, relative_base)
|
|
45
52
|
file_path = File.join(relative_base, "#{item[:name]}#{Constants::MARKDOWN_EXTENSION}")
|
|
46
53
|
full_file_path = File.join(docs_path, file_path)
|
|
@@ -50,11 +50,7 @@ module Docyard
|
|
|
50
50
|
def config_sidebar_items
|
|
51
51
|
return [] unless config
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
config.dig("sidebar", "items") || config.dig(:sidebar, :items) || []
|
|
55
|
-
else
|
|
56
|
-
config.sidebar&.items || []
|
|
57
|
-
end
|
|
53
|
+
config.sidebar&.items || []
|
|
58
54
|
end
|
|
59
55
|
|
|
60
56
|
def config_parser
|
|
@@ -84,19 +80,11 @@ module Docyard
|
|
|
84
80
|
end
|
|
85
81
|
|
|
86
82
|
def extract_base_url
|
|
87
|
-
|
|
88
|
-
config.dig(:build, :base_url) || "/"
|
|
89
|
-
else
|
|
90
|
-
config&.build&.base_url || "/"
|
|
91
|
-
end
|
|
83
|
+
config&.build&.base_url || "/"
|
|
92
84
|
end
|
|
93
85
|
|
|
94
86
|
def extract_site_title
|
|
95
|
-
|
|
96
|
-
config[:site_title] || "Documentation"
|
|
97
|
-
else
|
|
98
|
-
config&.site&.title || "Documentation"
|
|
99
|
-
end
|
|
87
|
+
config&.site&.title || "Documentation"
|
|
100
88
|
end
|
|
101
89
|
end
|
|
102
90
|
end
|
|
@@ -36,7 +36,10 @@ module Docyard
|
|
|
36
36
|
"siren" => '<path d="M120,16V8a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0Zm80,32a8,8,0,0,0,5.66-2.34l8-8a8,8,0,0,0-11.32-11.32l-8,8A8,8,0,0,0,200,48ZM50.34,45.66A8,8,0,0,0,61.66,34.34l-8-8A8,8,0,0,0,42.34,37.66Zm87,26.45a8,8,0,1,0-2.64,15.78C153.67,91.08,168,108.32,168,128a8,8,0,0,0,16,0C184,100.6,163.93,76.57,137.32,72.11ZM232,176v24a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V176a16,16,0,0,1,16-16V128a88,88,0,0,1,88.67-88c48.15.36,87.33,40.29,87.33,89v31A16,16,0,0,1,232,176ZM56,160H200V129c0-40-32.05-72.71-71.45-73H128a72,72,0,0,0-72,72Zm160,40V176H40v24H216Z"/>',
|
|
37
37
|
"file" => '<path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Z"/>',
|
|
38
38
|
"terminal-window" => '<path d="M128,128a8,8,0,0,1-3,6.25l-40,32a8,8,0,1,1-10-12.5L107.19,128,75,102.25a8,8,0,1,1,10-12.5l40,32A8,8,0,0,1,128,128Zm48,24H136a8,8,0,0,0,0,16h40a8,8,0,0,0,0-16Zm56-96V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM216,200V56H40V200H216Z"/>',
|
|
39
|
-
"list-dashes" => '<path d="M88,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H96A8,8,0,0,1,88,64Zm128,56H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM56,56H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Z"/>'
|
|
39
|
+
"list-dashes" => '<path d="M88,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H96A8,8,0,0,1,88,64Zm128,56H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM56,56H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Z"/>',
|
|
40
|
+
"magnifying-glass" => '<path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>',
|
|
41
|
+
"command" => '<path d="M180,144H160V112h20a36,36,0,1,0-36-36V96H112V76a36,36,0,1,0-36,36H96v32H76a36,36,0,1,0,36,36V160h32v20a36,36,0,1,0,36-36ZM160,76a20,20,0,1,1,20,20H160ZM56,76a20,20,0,0,1,40,0V96H76A20,20,0,0,1,56,76ZM96,180a20,20,0,1,1-20-20H96Zm16-68h32v32H112Zm68,88a20,20,0,0,1-20-20V160h20a20,20,0,0,1,0,40Z"/>',
|
|
42
|
+
"hash" => '<path d="M224,88H175.4l8.47-46.57a8,8,0,0,0-15.74-2.86l-9,49.43H111.4l8.47-46.57a8,8,0,0,0-15.74-2.86L95.14,88H48a8,8,0,0,0,0,16H92.23L83.5,152H32a8,8,0,0,0,0,16H80.6l-8.47,46.57a8,8,0,0,0,6.44,9.3A7.79,7.79,0,0,0,80,224a8,8,0,0,0,7.86-6.57l9-49.43H144.6l-8.47,46.57a8,8,0,0,0,6.44,9.3,7.79,7.79,0,0,0,1.43.13,8,8,0,0,0,7.86-6.57l9-49.43H208a8,8,0,0,0,0-16H163.77l8.73-48H224a8,8,0,0,0,0-16Zm-68.5,64H107.77l8.73-48h47.73Z"/>'
|
|
40
43
|
},
|
|
41
44
|
"bold" => {
|
|
42
45
|
"heart" => '<path d="M178,36c-20.09,0-37.92,7.93-50,21.56C115.92,43.93,98.09,36,78,36a66.08,66.08,0,0,0-66,66c0,72.34,105.81,130.14,110.31,132.57a12,12,0,0,0,11.38,0C138.19,232.14,244,174.34,244,102A66.08,66.08,0,0,0,178,36Zm-5.49,142.36A328.69,328.69,0,0,1,128,210.16a328.69,328.69,0,0,1-44.51-31.8C61.82,159.77,36,131.42,36,102A42,42,0,0,1,78,60c17.8,0,32.7,9.4,38.89,24.54a12,12,0,0,0,22.22,0C145.3,69.4,160.2,60,178,60a42,42,0,0,1,42,42C220,131.42,194.18,159.77,172.51,178.36Z"/>'
|
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
require "kramdown"
|
|
4
4
|
require "kramdown-parser-gfm"
|
|
5
5
|
require "yaml"
|
|
6
|
-
require_relative "components/registry"
|
|
7
|
-
require_relative "components/base_processor"
|
|
8
|
-
require_relative "components/callout_processor"
|
|
9
|
-
require_relative "components/tabs_processor"
|
|
10
|
-
require_relative "components/icon_processor"
|
|
11
|
-
require_relative "components/code_block_processor"
|
|
12
|
-
require_relative "components/code_snippet_import_preprocessor"
|
|
13
|
-
require_relative "components/code_block_options_preprocessor"
|
|
14
|
-
require_relative "components/code_block_diff_preprocessor"
|
|
15
|
-
require_relative "components/code_block_focus_preprocessor"
|
|
16
|
-
require_relative "components/table_wrapper_processor"
|
|
17
|
-
require_relative "components/heading_anchor_processor"
|
|
18
|
-
require_relative "components/table_of_contents_processor"
|
|
6
|
+
require_relative "../components/registry"
|
|
7
|
+
require_relative "../components/base_processor"
|
|
8
|
+
require_relative "../components/processors/callout_processor"
|
|
9
|
+
require_relative "../components/processors/tabs_processor"
|
|
10
|
+
require_relative "../components/processors/icon_processor"
|
|
11
|
+
require_relative "../components/processors/code_block_processor"
|
|
12
|
+
require_relative "../components/processors/code_snippet_import_preprocessor"
|
|
13
|
+
require_relative "../components/processors/code_block_options_preprocessor"
|
|
14
|
+
require_relative "../components/processors/code_block_diff_preprocessor"
|
|
15
|
+
require_relative "../components/processors/code_block_focus_preprocessor"
|
|
16
|
+
require_relative "../components/processors/table_wrapper_processor"
|
|
17
|
+
require_relative "../components/processors/heading_anchor_processor"
|
|
18
|
+
require_relative "../components/processors/table_of_contents_processor"
|
|
19
|
+
require_relative "../components/aliases"
|
|
19
20
|
|
|
20
21
|
module Docyard
|
|
21
22
|
class Markdown
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
|
-
require_relative "constants"
|
|
4
|
+
require_relative "../config/constants"
|
|
5
5
|
|
|
6
6
|
module Docyard
|
|
7
7
|
class Renderer
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
include Utils::UrlHelpers
|
|
9
|
+
|
|
10
|
+
LAYOUTS_PATH = File.join(__dir__, "../templates", "layouts")
|
|
11
|
+
ERRORS_PATH = File.join(__dir__, "../templates", "errors")
|
|
12
|
+
PARTIALS_PATH = File.join(__dir__, "../templates", "partials")
|
|
11
13
|
|
|
12
14
|
attr_reader :layout_path, :base_url, :config
|
|
13
15
|
|
|
@@ -80,21 +82,8 @@ module Docyard
|
|
|
80
82
|
"#{base_url}#{path}"
|
|
81
83
|
end
|
|
82
84
|
|
|
83
|
-
def link_path(path)
|
|
84
|
-
return path if path.nil? || path.start_with?("http://", "https://")
|
|
85
|
-
|
|
86
|
-
"#{base_url.chomp('/')}#{path}"
|
|
87
|
-
end
|
|
88
|
-
|
|
89
85
|
private
|
|
90
86
|
|
|
91
|
-
def normalize_base_url(url)
|
|
92
|
-
return "/" if url.nil? || url.empty?
|
|
93
|
-
|
|
94
|
-
url = "/#{url}" unless url.start_with?("/")
|
|
95
|
-
url.end_with?("/") ? url : "#{url}/"
|
|
96
|
-
end
|
|
97
|
-
|
|
98
87
|
def assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
|
|
99
88
|
@content = content
|
|
100
89
|
@page_title = page_title
|
|
@@ -104,15 +93,29 @@ module Docyard
|
|
|
104
93
|
end
|
|
105
94
|
|
|
106
95
|
def assign_branding_variables(branding)
|
|
96
|
+
assign_site_branding(branding)
|
|
97
|
+
assign_display_options(branding)
|
|
98
|
+
assign_search_options(branding)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def assign_site_branding(branding)
|
|
107
102
|
@site_title = branding[:site_title] || Constants::DEFAULT_SITE_TITLE
|
|
108
103
|
@site_description = branding[:site_description] || ""
|
|
109
104
|
@logo = branding[:logo] || Constants::DEFAULT_LOGO_PATH
|
|
110
105
|
@logo_dark = branding[:logo_dark]
|
|
111
106
|
@favicon = branding[:favicon] || Constants::DEFAULT_FAVICON_PATH
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def assign_display_options(branding)
|
|
112
110
|
@display_logo = branding[:display_logo].nil? || branding[:display_logo]
|
|
113
111
|
@display_title = branding[:display_title].nil? || branding[:display_title]
|
|
114
112
|
end
|
|
115
113
|
|
|
114
|
+
def assign_search_options(branding)
|
|
115
|
+
@search_enabled = branding[:search_enabled].nil? || branding[:search_enabled]
|
|
116
|
+
@search_placeholder = branding[:search_placeholder] || "Search documentation..."
|
|
117
|
+
end
|
|
118
|
+
|
|
116
119
|
def strip_md_from_links(html)
|
|
117
120
|
html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
|
|
118
121
|
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_dir
|
|
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,110 @@
|
|
|
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
|
+
run_pagefind
|
|
29
|
+
@pagefind_path = File.join(temp_dir, "pagefind")
|
|
30
|
+
|
|
31
|
+
log_success
|
|
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
|
+
renderer = Renderer.new(base_url: "/", config: config)
|
|
56
|
+
|
|
57
|
+
progress = TTY::ProgressBar.new(
|
|
58
|
+
"Indexing search [:bar] :current/:total (:percent)",
|
|
59
|
+
total: markdown_files.size,
|
|
60
|
+
width: 50
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
markdown_files.each do |file_path|
|
|
64
|
+
generate_html_file(file_path, renderer)
|
|
65
|
+
progress.advance
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def generate_html_file(markdown_file, renderer)
|
|
70
|
+
relative_path = markdown_file.delete_prefix("#{docs_path}/")
|
|
71
|
+
output_path = determine_output_path(relative_path)
|
|
72
|
+
|
|
73
|
+
html = renderer.render_file(markdown_file, branding: branding_options)
|
|
74
|
+
|
|
75
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
76
|
+
File.write(output_path, html)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def determine_output_path(relative_path)
|
|
80
|
+
base_name = File.basename(relative_path, ".md")
|
|
81
|
+
dir_name = File.dirname(relative_path)
|
|
82
|
+
|
|
83
|
+
if base_name == "index"
|
|
84
|
+
File.join(temp_dir, dir_name, "index.html")
|
|
85
|
+
else
|
|
86
|
+
File.join(temp_dir, dir_name, base_name, "index.html")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def branding_options
|
|
91
|
+
BrandingResolver.new(config).resolve
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def run_pagefind
|
|
95
|
+
args = build_pagefind_args(temp_dir)
|
|
96
|
+
stdout, stderr, status = Open3.capture3("npx", *args)
|
|
97
|
+
|
|
98
|
+
raise "Pagefind failed: #{stderr}" unless status.success?
|
|
99
|
+
|
|
100
|
+
stdout
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def log_success
|
|
104
|
+
page_count = Dir.glob(File.join(temp_dir, "**", "*.html")).size
|
|
105
|
+
puts "=> Search index generated (#{page_count} pages)"
|
|
106
|
+
puts "=> Temp directory: #{temp_dir}" if ENV["DOCYARD_DEBUG"]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
args += ["--exclude-selectors", pattern]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
args
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -4,30 +4,30 @@ require "webrick"
|
|
|
4
4
|
require "stringio"
|
|
5
5
|
require_relative "file_watcher"
|
|
6
6
|
require_relative "rack_application"
|
|
7
|
-
require_relative "config"
|
|
7
|
+
require_relative "../config"
|
|
8
8
|
|
|
9
9
|
module Docyard
|
|
10
10
|
class Server
|
|
11
11
|
DEFAULT_PORT = 4200
|
|
12
12
|
DEFAULT_HOST = "localhost"
|
|
13
13
|
|
|
14
|
-
attr_reader :port, :host, :docs_path, :config
|
|
14
|
+
attr_reader :port, :host, :docs_path, :config, :search_enabled
|
|
15
15
|
|
|
16
|
-
def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, docs_path: "docs")
|
|
16
|
+
def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, docs_path: "docs", search: false)
|
|
17
17
|
@port = port
|
|
18
18
|
@host = host
|
|
19
19
|
@docs_path = docs_path
|
|
20
|
+
@search_enabled = search
|
|
20
21
|
@config = Config.load
|
|
21
22
|
@file_watcher = FileWatcher.new(File.expand_path(docs_path))
|
|
22
|
-
@
|
|
23
|
-
|
|
24
|
-
file_watcher: @file_watcher,
|
|
25
|
-
config: @config
|
|
26
|
-
)
|
|
23
|
+
@search_indexer = nil
|
|
24
|
+
@app = nil
|
|
27
25
|
end
|
|
28
26
|
|
|
29
27
|
def start
|
|
30
28
|
validate_docs_directory!
|
|
29
|
+
generate_search_index if @search_enabled
|
|
30
|
+
initialize_app
|
|
31
31
|
print_server_info
|
|
32
32
|
@file_watcher.start
|
|
33
33
|
|
|
@@ -35,11 +35,33 @@ module Docyard
|
|
|
35
35
|
trap("INT") { shutdown_server }
|
|
36
36
|
|
|
37
37
|
http_server.start
|
|
38
|
-
|
|
38
|
+
cleanup
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
+
def generate_search_index
|
|
44
|
+
@search_indexer = Search::DevIndexer.new(
|
|
45
|
+
docs_path: File.expand_path(docs_path),
|
|
46
|
+
config: @config
|
|
47
|
+
)
|
|
48
|
+
@search_indexer.generate
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize_app
|
|
52
|
+
@app = RackApplication.new(
|
|
53
|
+
docs_path: File.expand_path(docs_path),
|
|
54
|
+
file_watcher: @file_watcher,
|
|
55
|
+
config: @config,
|
|
56
|
+
pagefind_path: @search_indexer&.pagefind_path
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cleanup
|
|
61
|
+
@file_watcher.stop
|
|
62
|
+
@search_indexer&.cleanup
|
|
63
|
+
end
|
|
64
|
+
|
|
43
65
|
def validate_docs_directory!
|
|
44
66
|
return if File.directory?(docs_path)
|
|
45
67
|
|
|
@@ -51,6 +73,7 @@ module Docyard
|
|
|
51
73
|
puts "Starting Docyard server..."
|
|
52
74
|
puts "=> Serving docs from: #{docs_path}/"
|
|
53
75
|
puts "=> Running at: http://#{host}:#{port}"
|
|
76
|
+
puts "=> Search: #{@search_enabled ? 'enabled' : 'disabled (use --search to enable)'}"
|
|
54
77
|
puts "=> Press Ctrl+C to stop\n"
|
|
55
78
|
end
|
|
56
79
|
|
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "rack"
|
|
5
|
-
require_relative "sidebar_builder"
|
|
6
|
-
require_relative "prev_next_builder"
|
|
7
|
-
require_relative "
|
|
5
|
+
require_relative "../navigation/sidebar_builder"
|
|
6
|
+
require_relative "../navigation/prev_next_builder"
|
|
7
|
+
require_relative "../config/branding_resolver"
|
|
8
|
+
require_relative "../config/constants"
|
|
8
9
|
|
|
9
10
|
module Docyard
|
|
10
11
|
class RackApplication
|
|
11
|
-
|
|
12
|
+
PAGEFIND_CONTENT_TYPES = {
|
|
13
|
+
".js" => "application/javascript; charset=utf-8",
|
|
14
|
+
".css" => "text/css; charset=utf-8",
|
|
15
|
+
".json" => "application/json; charset=utf-8"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(docs_path:, file_watcher:, config: nil, pagefind_path: nil)
|
|
12
19
|
@docs_path = docs_path
|
|
13
20
|
@file_watcher = file_watcher
|
|
14
21
|
@config = config
|
|
22
|
+
@pagefind_path = pagefind_path
|
|
15
23
|
@router = Router.new(docs_path: docs_path)
|
|
16
24
|
@renderer = Renderer.new(base_url: config&.build&.base_url || "/", config: config)
|
|
17
25
|
@asset_handler = AssetHandler.new
|
|
@@ -23,13 +31,14 @@ module Docyard
|
|
|
23
31
|
|
|
24
32
|
private
|
|
25
33
|
|
|
26
|
-
attr_reader :docs_path, :file_watcher, :config, :router, :renderer, :asset_handler
|
|
34
|
+
attr_reader :docs_path, :file_watcher, :config, :pagefind_path, :router, :renderer, :asset_handler
|
|
27
35
|
|
|
28
36
|
def handle_request(env)
|
|
29
37
|
path = env["PATH_INFO"]
|
|
30
38
|
|
|
31
39
|
return handle_reload_check(env) if path == Constants::RELOAD_ENDPOINT
|
|
32
40
|
return asset_handler.serve(path) if path.start_with?(Constants::ASSETS_PREFIX)
|
|
41
|
+
return serve_pagefind(path) if path.start_with?(Constants::PAGEFIND_PREFIX)
|
|
33
42
|
|
|
34
43
|
handle_documentation_request(path)
|
|
35
44
|
rescue StandardError => e
|
|
@@ -91,50 +100,7 @@ module Docyard
|
|
|
91
100
|
end
|
|
92
101
|
|
|
93
102
|
def branding_options
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
default_branding.merge(config_branding_options)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def default_branding
|
|
100
|
-
{
|
|
101
|
-
site_title: Constants::DEFAULT_SITE_TITLE,
|
|
102
|
-
site_description: "",
|
|
103
|
-
logo: Constants::DEFAULT_LOGO_PATH,
|
|
104
|
-
logo_dark: Constants::DEFAULT_LOGO_DARK_PATH,
|
|
105
|
-
favicon: nil,
|
|
106
|
-
display_logo: true,
|
|
107
|
-
display_title: true
|
|
108
|
-
}
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def config_branding_options
|
|
112
|
-
site = config.site
|
|
113
|
-
branding = config.branding
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
site_title: site.title || Constants::DEFAULT_SITE_TITLE,
|
|
117
|
-
site_description: site.description || "",
|
|
118
|
-
logo: resolve_logo(branding.logo, branding.logo_dark),
|
|
119
|
-
logo_dark: resolve_logo_dark(branding.logo, branding.logo_dark),
|
|
120
|
-
favicon: branding.favicon
|
|
121
|
-
}.merge(appearance_options(branding.appearance))
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def appearance_options(appearance)
|
|
125
|
-
appearance ||= {}
|
|
126
|
-
{
|
|
127
|
-
display_logo: appearance["logo"] != false,
|
|
128
|
-
display_title: appearance["title"] != false
|
|
129
|
-
}
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def resolve_logo(logo, logo_dark)
|
|
133
|
-
logo || logo_dark || Constants::DEFAULT_LOGO_PATH
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def resolve_logo_dark(logo, logo_dark)
|
|
137
|
-
logo_dark || logo || Constants::DEFAULT_LOGO_DARK_PATH
|
|
103
|
+
BrandingResolver.new(config).resolve
|
|
138
104
|
end
|
|
139
105
|
|
|
140
106
|
def handle_reload_check(env)
|
|
@@ -168,5 +134,42 @@ module Docyard
|
|
|
168
134
|
[Constants::STATUS_INTERNAL_ERROR, { "Content-Type" => Constants::CONTENT_TYPE_HTML },
|
|
169
135
|
[renderer.render_server_error(error)]]
|
|
170
136
|
end
|
|
137
|
+
|
|
138
|
+
def serve_pagefind(path)
|
|
139
|
+
relative_path = path.delete_prefix(Constants::PAGEFIND_PREFIX)
|
|
140
|
+
return pagefind_not_found if relative_path.include?("..")
|
|
141
|
+
|
|
142
|
+
file_path = resolve_pagefind_file(relative_path)
|
|
143
|
+
return pagefind_not_found unless file_path && File.file?(file_path)
|
|
144
|
+
|
|
145
|
+
content = File.binread(file_path)
|
|
146
|
+
content_type = pagefind_content_type(file_path)
|
|
147
|
+
|
|
148
|
+
headers = {
|
|
149
|
+
"Content-Type" => content_type,
|
|
150
|
+
"Cache-Control" => "no-cache, no-store, must-revalidate",
|
|
151
|
+
"Pragma" => "no-cache",
|
|
152
|
+
"Expires" => "0"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
[Constants::STATUS_OK, headers, [content]]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def resolve_pagefind_file(relative_path)
|
|
159
|
+
return File.join(pagefind_path, relative_path) if pagefind_path && Dir.exist?(pagefind_path)
|
|
160
|
+
|
|
161
|
+
output_dir = config&.build&.output_dir || "dist"
|
|
162
|
+
File.join(output_dir, "pagefind", relative_path)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def pagefind_content_type(file_path)
|
|
166
|
+
extension = File.extname(file_path)
|
|
167
|
+
PAGEFIND_CONTENT_TYPES.fetch(extension, "application/octet-stream")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def pagefind_not_found
|
|
171
|
+
message = "Pagefind not found. Run 'docyard build' first."
|
|
172
|
+
[Constants::STATUS_NOT_FOUND, { "Content-Type" => "text/plain" }, [message]]
|
|
173
|
+
end
|
|
171
174
|
end
|
|
172
175
|
end
|