docyard 0.9.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 +43 -0
- 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/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/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 +87 -59
- 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 +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/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/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 +7 -4
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
- 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 +17 -0
- data/lib/docyard/templates/assets/css/markdown.css +22 -2
- data/lib/docyard/templates/assets/css/variables.css +13 -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 +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 +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 +79 -4
- 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 +77 -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
|
@@ -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
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "
|
|
5
|
-
|
|
3
|
+
require "puma"
|
|
4
|
+
require "puma/configuration"
|
|
5
|
+
require "puma/launcher"
|
|
6
|
+
require "puma/log_writer"
|
|
6
7
|
require_relative "rack_application"
|
|
8
|
+
require_relative "sse_server"
|
|
9
|
+
require_relative "file_watcher"
|
|
7
10
|
require_relative "../config"
|
|
11
|
+
require_relative "../navigation/sidebar/cache"
|
|
8
12
|
|
|
9
13
|
module Docyard
|
|
10
|
-
class
|
|
14
|
+
class DevServer
|
|
11
15
|
DEFAULT_PORT = 4200
|
|
12
16
|
DEFAULT_HOST = "localhost"
|
|
13
17
|
|
|
@@ -19,27 +23,34 @@ module Docyard
|
|
|
19
23
|
@docs_path = docs_path
|
|
20
24
|
@search_enabled = search
|
|
21
25
|
@config = Config.load
|
|
22
|
-
@file_watcher = FileWatcher.new(File.expand_path(docs_path))
|
|
23
26
|
@search_indexer = nil
|
|
24
|
-
@
|
|
27
|
+
@sse_server = nil
|
|
28
|
+
@file_watcher = nil
|
|
29
|
+
@launcher = nil
|
|
30
|
+
@sidebar_cache = nil
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
def start
|
|
28
34
|
validate_docs_directory!
|
|
35
|
+
build_sidebar_cache
|
|
29
36
|
generate_search_index if @search_enabled
|
|
30
|
-
|
|
37
|
+
setup_hot_reload
|
|
31
38
|
print_server_info
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
http_server.mount_proc("/") { |req, res| handle_request(req, res) }
|
|
35
|
-
trap("INT") { shutdown_server }
|
|
36
|
-
|
|
37
|
-
http_server.start
|
|
39
|
+
run_server
|
|
40
|
+
ensure
|
|
38
41
|
cleanup
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
private
|
|
42
45
|
|
|
46
|
+
def build_sidebar_cache
|
|
47
|
+
@sidebar_cache = Sidebar::Cache.new(
|
|
48
|
+
docs_path: File.expand_path(docs_path),
|
|
49
|
+
config: @config
|
|
50
|
+
)
|
|
51
|
+
@sidebar_cache.build
|
|
52
|
+
end
|
|
53
|
+
|
|
43
54
|
def generate_search_index
|
|
44
55
|
@search_indexer = Search::DevIndexer.new(
|
|
45
56
|
docs_path: File.expand_path(docs_path),
|
|
@@ -48,17 +59,45 @@ module Docyard
|
|
|
48
59
|
@search_indexer.generate
|
|
49
60
|
end
|
|
50
61
|
|
|
51
|
-
def
|
|
52
|
-
@
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
def setup_hot_reload
|
|
63
|
+
@sse_server = SSEServer.new(port: sse_port)
|
|
64
|
+
@sse_server.start
|
|
65
|
+
|
|
66
|
+
@file_watcher = FileWatcher.new(
|
|
67
|
+
docs_path: docs_path,
|
|
68
|
+
on_change: ->(change_type) { handle_file_change(change_type) }
|
|
57
69
|
)
|
|
70
|
+
@file_watcher.start
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def sse_port
|
|
74
|
+
port + 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def handle_file_change(change_type)
|
|
78
|
+
invalidate_sidebar_cache if change_type == :full
|
|
79
|
+
log_file_change(change_type)
|
|
80
|
+
@sse_server.broadcast("reload", { type: change_type.to_s })
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def invalidate_sidebar_cache
|
|
84
|
+
@sidebar_cache&.invalidate
|
|
85
|
+
@sidebar_cache&.build
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def log_file_change(change_type)
|
|
89
|
+
message = case change_type
|
|
90
|
+
when :content then "Content changed, reloading..."
|
|
91
|
+
when :config then "Config changed, full reload..."
|
|
92
|
+
when :asset then "Asset changed, reloading..."
|
|
93
|
+
else "File changed, reloading..."
|
|
94
|
+
end
|
|
95
|
+
Docyard.logger.info("* #{message}")
|
|
58
96
|
end
|
|
59
97
|
|
|
60
98
|
def cleanup
|
|
61
|
-
@file_watcher
|
|
99
|
+
@file_watcher&.stop
|
|
100
|
+
@sse_server&.stop
|
|
62
101
|
@search_indexer&.cleanup
|
|
63
102
|
end
|
|
64
103
|
|
|
@@ -70,48 +109,44 @@ module Docyard
|
|
|
70
109
|
end
|
|
71
110
|
|
|
72
111
|
def print_server_info
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
112
|
+
Docyard.logger.info("Starting Docyard server...")
|
|
113
|
+
Docyard.logger.info("* Version: #{Docyard::VERSION}")
|
|
114
|
+
Docyard.logger.info("* Running at: http://#{host}:#{port}")
|
|
115
|
+
Docyard.logger.info("* Hot reload: ws://127.0.0.1:#{sse_port}")
|
|
116
|
+
Docyard.logger.info("* Search: #{@search_enabled ? 'enabled' : 'disabled (use --search to enable)'}")
|
|
117
|
+
Docyard.logger.info("Use Ctrl+C to stop\n")
|
|
78
118
|
end
|
|
79
119
|
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
120
|
+
def run_server
|
|
121
|
+
app = build_rack_app
|
|
122
|
+
puma_config = build_puma_config(app)
|
|
123
|
+
log_writer = Puma::LogWriter.strings
|
|
84
124
|
|
|
85
|
-
|
|
86
|
-
@
|
|
87
|
-
Port: port,
|
|
88
|
-
BindAddress: host,
|
|
89
|
-
AccessLog: [],
|
|
90
|
-
Logger: WEBrick::Log.new(File::NULL)
|
|
91
|
-
)
|
|
125
|
+
@launcher = Puma::Launcher.new(puma_config, log_writer: log_writer)
|
|
126
|
+
@launcher.run
|
|
92
127
|
end
|
|
93
128
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
def build_rack_app
|
|
130
|
+
RackApplication.new(
|
|
131
|
+
docs_path: File.expand_path(docs_path),
|
|
132
|
+
config: @config,
|
|
133
|
+
pagefind_path: @search_indexer&.pagefind_path,
|
|
134
|
+
sse_port: sse_port,
|
|
135
|
+
sidebar_cache: @sidebar_cache
|
|
136
|
+
)
|
|
101
137
|
end
|
|
102
138
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
139
|
+
def build_puma_config(app)
|
|
140
|
+
server_host = host
|
|
141
|
+
server_port = port
|
|
142
|
+
|
|
143
|
+
Puma::Configuration.new do |config|
|
|
144
|
+
config.bind "tcp://#{server_host}:#{server_port}"
|
|
145
|
+
config.app app
|
|
146
|
+
config.workers 0
|
|
147
|
+
config.threads 4, 8
|
|
148
|
+
config.quiet
|
|
149
|
+
end
|
|
115
150
|
end
|
|
116
151
|
end
|
|
117
152
|
end
|
|
@@ -4,39 +4,89 @@ require "listen"
|
|
|
4
4
|
|
|
5
5
|
module Docyard
|
|
6
6
|
class FileWatcher
|
|
7
|
-
|
|
7
|
+
DEBOUNCE_DELAY = 0.1
|
|
8
|
+
ROOT_CONFIG_FILE = "docyard.yml"
|
|
9
|
+
CONFIG_FILES = %w[docyard.yml _sidebar.yml].freeze
|
|
10
|
+
CONTENT_EXTENSIONS = %w[.md .markdown].freeze
|
|
11
|
+
ASSET_EXTENSIONS = %w[.css .js .html .erb].freeze
|
|
8
12
|
|
|
9
|
-
def initialize(docs_path)
|
|
10
|
-
@docs_path = docs_path
|
|
11
|
-
@
|
|
12
|
-
@
|
|
13
|
+
def initialize(docs_path:, on_change:)
|
|
14
|
+
@docs_path = File.expand_path(docs_path)
|
|
15
|
+
@project_root = File.dirname(@docs_path)
|
|
16
|
+
@on_change = on_change
|
|
17
|
+
@docs_listener = nil
|
|
18
|
+
@config_listener = nil
|
|
19
|
+
@pending_changes = { content: false, config: false, asset: false }
|
|
20
|
+
@debounce_timer = nil
|
|
21
|
+
@mutex = Mutex.new
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
def start
|
|
16
|
-
@
|
|
17
|
-
handle_changes(modified
|
|
25
|
+
@docs_listener = Listen.to(@docs_path, latency: DEBOUNCE_DELAY) do |modified, added, removed|
|
|
26
|
+
handle_changes(modified + added + removed)
|
|
18
27
|
end
|
|
28
|
+
@docs_listener.start
|
|
19
29
|
|
|
20
|
-
@
|
|
30
|
+
@config_listener = Listen.to(@project_root, only: /\Adocyard\.yml\z/) do |modified, added, removed|
|
|
31
|
+
handle_changes(modified + added + removed)
|
|
32
|
+
end
|
|
33
|
+
@config_listener.start
|
|
21
34
|
end
|
|
22
35
|
|
|
23
36
|
def stop
|
|
24
|
-
@
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
@docs_listener&.stop
|
|
38
|
+
@config_listener&.stop
|
|
39
|
+
@debounce_timer&.kill
|
|
27
40
|
end
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def handle_changes(paths)
|
|
45
|
+
return if paths.empty?
|
|
46
|
+
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
paths.each { |path| categorize_change(path) }
|
|
49
|
+
schedule_notification
|
|
50
|
+
end
|
|
31
51
|
end
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
def categorize_change(path)
|
|
54
|
+
filename = File.basename(path)
|
|
55
|
+
|
|
56
|
+
if CONFIG_FILES.include?(filename)
|
|
57
|
+
@pending_changes[:config] = true
|
|
58
|
+
elsif CONTENT_EXTENSIONS.include?(File.extname(path))
|
|
59
|
+
@pending_changes[:content] = true
|
|
60
|
+
elsif ASSET_EXTENSIONS.include?(File.extname(path))
|
|
61
|
+
@pending_changes[:asset] = true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def schedule_notification
|
|
66
|
+
@debounce_timer&.kill
|
|
67
|
+
@debounce_timer = Thread.new do
|
|
68
|
+
sleep DEBOUNCE_DELAY
|
|
69
|
+
send_notification
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def send_notification
|
|
74
|
+
changes = nil
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
changes = @pending_changes.dup
|
|
77
|
+
@pending_changes = { content: false, config: false, asset: false }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
change_type = determine_change_type(changes)
|
|
81
|
+
@on_change.call(change_type) if change_type
|
|
82
|
+
end
|
|
34
83
|
|
|
35
|
-
def
|
|
36
|
-
return if
|
|
84
|
+
def determine_change_type(changes)
|
|
85
|
+
return :full if changes[:config]
|
|
86
|
+
return :content if changes[:content]
|
|
87
|
+
return :asset if changes[:asset]
|
|
37
88
|
|
|
38
|
-
|
|
39
|
-
Docyard.logger.info "Files changed, triggering reload..."
|
|
89
|
+
nil
|
|
40
90
|
end
|
|
41
91
|
end
|
|
42
92
|
end
|
|
@@ -31,7 +31,7 @@ module Docyard
|
|
|
31
31
|
return File.join(pagefind_path, relative_path) if pagefind_path && Dir.exist?(pagefind_path)
|
|
32
32
|
|
|
33
33
|
output_dir = config&.build&.output_dir || "dist"
|
|
34
|
-
File.join(output_dir, "pagefind", relative_path)
|
|
34
|
+
File.join(output_dir, "_docyard", "pagefind", relative_path)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def serve_file(file_path)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "puma"
|
|
4
|
+
require "puma/configuration"
|
|
5
|
+
require "puma/launcher"
|
|
6
|
+
require "puma/log_writer"
|
|
4
7
|
require_relative "../config"
|
|
8
|
+
require_relative "static_file_app"
|
|
5
9
|
|
|
6
10
|
module Docyard
|
|
7
11
|
class PreviewServer
|
|
@@ -13,16 +17,13 @@ module Docyard
|
|
|
13
17
|
@port = port
|
|
14
18
|
@config = Config.load
|
|
15
19
|
@output_dir = File.expand_path(@config.build.output)
|
|
20
|
+
@launcher = nil
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
def start
|
|
19
24
|
validate_output_directory!
|
|
20
25
|
print_server_info
|
|
21
|
-
|
|
22
|
-
server = create_server
|
|
23
|
-
trap("INT") { shutdown_server(server) }
|
|
24
|
-
|
|
25
|
-
server.start
|
|
26
|
+
run_server
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
private
|
|
@@ -35,38 +36,33 @@ module Docyard
|
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def print_server_info
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
Docyard.logger.info("Starting preview server...")
|
|
40
|
+
Docyard.logger.info("* Version: #{Docyard::VERSION}")
|
|
41
|
+
Docyard.logger.info("* Running at: http://localhost:#{port}")
|
|
42
|
+
Docyard.logger.info("Use Ctrl+C to stop\n")
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
AccessLog: [],
|
|
49
|
-
Logger: WEBrick::Log.new(File::NULL),
|
|
50
|
-
MimeTypes: mime_types
|
|
51
|
-
)
|
|
52
|
-
end
|
|
45
|
+
def run_server
|
|
46
|
+
app = StaticFileApp.new(output_dir)
|
|
47
|
+
puma_config = build_puma_config(app)
|
|
48
|
+
log_writer = Puma::LogWriter.strings
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"js" => "application/javascript",
|
|
59
|
-
"json" => "application/json",
|
|
60
|
-
"svg" => "image/svg+xml",
|
|
61
|
-
"woff" => "font/woff",
|
|
62
|
-
"woff2" => "font/woff2"
|
|
63
|
-
}
|
|
64
|
-
)
|
|
50
|
+
@launcher = Puma::Launcher.new(puma_config, log_writer: log_writer)
|
|
51
|
+
@launcher.run
|
|
52
|
+
rescue Interrupt
|
|
53
|
+
Docyard.logger.info("\nShutting down preview server...")
|
|
65
54
|
end
|
|
66
55
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
def build_puma_config(app)
|
|
57
|
+
server_port = port
|
|
58
|
+
|
|
59
|
+
Puma::Configuration.new do |config|
|
|
60
|
+
config.bind "tcp://localhost:#{server_port}"
|
|
61
|
+
config.app app
|
|
62
|
+
config.workers 0
|
|
63
|
+
config.threads 1, 4
|
|
64
|
+
config.quiet
|
|
65
|
+
end
|
|
70
66
|
end
|
|
71
67
|
end
|
|
72
68
|
end
|