docyard 1.0.1 → 1.1.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 +36 -1
- data/lib/docyard/build/asset_bundler.rb +9 -34
- data/lib/docyard/build/file_copier.rb +7 -15
- data/lib/docyard/build/llms_txt_generator.rb +0 -2
- data/lib/docyard/build/sitemap_generator.rb +1 -1
- data/lib/docyard/build/static_generator.rb +32 -33
- data/lib/docyard/build/step_runner.rb +88 -0
- data/lib/docyard/build/validator.rb +98 -0
- data/lib/docyard/builder.rb +82 -55
- data/lib/docyard/cli.rb +36 -4
- data/lib/docyard/components/aliases.rb +0 -4
- data/lib/docyard/components/processors/callout_processor.rb +1 -1
- data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +1 -1
- data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +1 -1
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +2 -2
- data/lib/docyard/components/processors/code_group_processor.rb +1 -1
- data/lib/docyard/components/processors/icon_processor.rb +2 -2
- data/lib/docyard/components/processors/tabs_processor.rb +1 -1
- data/lib/docyard/config/schema/definition.rb +29 -0
- data/lib/docyard/config/schema/sections.rb +63 -0
- data/lib/docyard/config/schema/simple_sections.rb +78 -0
- data/lib/docyard/config/schema.rb +28 -31
- data/lib/docyard/config/type_validators.rb +121 -0
- data/lib/docyard/config/validator.rb +136 -61
- data/lib/docyard/config.rb +1 -13
- data/lib/docyard/diagnostic.rb +89 -0
- data/lib/docyard/diagnostic_context.rb +48 -0
- data/lib/docyard/doctor/code_block_checker.rb +136 -0
- data/lib/docyard/doctor/component_checker.rb +49 -0
- data/lib/docyard/doctor/component_checkers/abbreviation_checker.rb +74 -0
- data/lib/docyard/doctor/component_checkers/badge_checker.rb +71 -0
- data/lib/docyard/doctor/component_checkers/base.rb +111 -0
- data/lib/docyard/doctor/component_checkers/callout_checker.rb +34 -0
- data/lib/docyard/doctor/component_checkers/cards_checker.rb +57 -0
- data/lib/docyard/doctor/component_checkers/code_group_checker.rb +47 -0
- data/lib/docyard/doctor/component_checkers/details_checker.rb +51 -0
- data/lib/docyard/doctor/component_checkers/icon_checker.rb +36 -0
- data/lib/docyard/doctor/component_checkers/image_attrs_checker.rb +46 -0
- data/lib/docyard/doctor/component_checkers/space_after_colons_checker.rb +45 -0
- data/lib/docyard/doctor/component_checkers/steps_checker.rb +35 -0
- data/lib/docyard/doctor/component_checkers/tabs_checker.rb +35 -0
- data/lib/docyard/doctor/component_checkers/tooltip_checker.rb +67 -0
- data/lib/docyard/doctor/component_checkers/unknown_type_checker.rb +34 -0
- data/lib/docyard/doctor/config_checker.rb +19 -0
- data/lib/docyard/doctor/config_fixer.rb +87 -0
- data/lib/docyard/doctor/content_checker.rb +164 -0
- data/lib/docyard/doctor/file_scanner.rb +113 -0
- data/lib/docyard/doctor/image_checker.rb +103 -0
- data/lib/docyard/doctor/link_checker.rb +91 -0
- data/lib/docyard/doctor/markdown_fixer.rb +62 -0
- data/lib/docyard/doctor/orphan_checker.rb +82 -0
- data/lib/docyard/doctor/reporter.rb +152 -0
- data/lib/docyard/doctor/sidebar_checker.rb +127 -0
- data/lib/docyard/doctor/sidebar_fixer.rb +47 -0
- data/lib/docyard/doctor.rb +178 -0
- data/lib/docyard/editor_launcher.rb +119 -0
- data/lib/docyard/errors.rb +0 -49
- data/lib/docyard/initializer.rb +32 -39
- data/lib/docyard/navigation/page_navigation_builder.rb +5 -3
- data/lib/docyard/navigation/prev_next_builder.rb +4 -3
- data/lib/docyard/navigation/sidebar/local_config_loader.rb +44 -21
- data/lib/docyard/rendering/icon_helpers.rb +1 -3
- data/lib/docyard/rendering/renderer.rb +17 -2
- data/lib/docyard/search/build_indexer.rb +39 -24
- data/lib/docyard/search/dev_indexer.rb +9 -23
- data/lib/docyard/server/dev_server.rb +55 -13
- data/lib/docyard/server/error_overlay.rb +73 -0
- data/lib/docyard/server/file_watcher.rb +0 -1
- data/lib/docyard/server/page_diagnostics.rb +27 -0
- data/lib/docyard/server/preview_server.rb +20 -8
- data/lib/docyard/server/rack_application.rb +64 -3
- data/lib/docyard/server/resolution_result.rb +0 -4
- data/lib/docyard/server/static_file_app.rb +20 -3
- data/lib/docyard/templates/assets/css/error-overlay.css +669 -0
- data/lib/docyard/templates/assets/css/variables.css +1 -1
- data/lib/docyard/templates/assets/fonts/Inter-Variable.woff2 +0 -0
- data/lib/docyard/templates/assets/js/components/relative-time.js +42 -0
- data/lib/docyard/templates/assets/js/error-overlay.js +547 -0
- data/lib/docyard/templates/assets/js/hot-reload.js +35 -7
- data/lib/docyard/templates/errors/404.html.erb +1 -1
- data/lib/docyard/templates/errors/500.html.erb +1 -1
- data/lib/docyard/templates/partials/_head.html.erb +1 -1
- data/lib/docyard/templates/partials/_page_actions.html.erb +1 -1
- data/lib/docyard/ui.rb +80 -0
- data/lib/docyard/utils/logging.rb +5 -1
- data/lib/docyard/utils/text_formatter.rb +0 -6
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +4 -0
- metadata +54 -29
- data/lib/docyard/config/key_validator.rb +0 -30
- data/lib/docyard/config/validation_helpers.rb +0 -83
- data/lib/docyard/config/validators/navigation.rb +0 -43
- data/lib/docyard/config/validators/section.rb +0 -114
- data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
|
@@ -18,39 +18,26 @@ module Docyard
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
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
|
|
21
|
+
return [0, nil] unless search_enabled?
|
|
22
|
+
return [0, nil] unless pagefind_available?
|
|
29
23
|
|
|
30
24
|
run_pagefind
|
|
31
25
|
end
|
|
32
26
|
|
|
33
27
|
private
|
|
34
28
|
|
|
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
29
|
def run_pagefind
|
|
42
30
|
args = build_pagefind_args(output_dir)
|
|
43
|
-
log "Running: npx #{args.join(' ')}" if verbose
|
|
44
31
|
|
|
45
32
|
stdout, stderr, status = Open3.capture3(PAGEFIND_COMMAND, *args)
|
|
46
33
|
|
|
47
34
|
if status.success?
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
count = extract_page_count(stdout)
|
|
36
|
+
details = verbose ? collect_index_details : nil
|
|
37
|
+
[count, details]
|
|
51
38
|
else
|
|
52
|
-
|
|
53
|
-
0
|
|
39
|
+
Docyard.logger.warn("Search indexing failed: #{stderr}") if verbose
|
|
40
|
+
[0, nil]
|
|
54
41
|
end
|
|
55
42
|
end
|
|
56
43
|
|
|
@@ -62,12 +49,40 @@ module Docyard
|
|
|
62
49
|
end
|
|
63
50
|
end
|
|
64
51
|
|
|
65
|
-
def
|
|
66
|
-
|
|
52
|
+
def collect_index_details
|
|
53
|
+
indexed, excluded = classify_pages
|
|
54
|
+
format_index_details(indexed, excluded)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def classify_pages
|
|
58
|
+
indexed = []
|
|
59
|
+
excluded = []
|
|
60
|
+
|
|
61
|
+
Dir.glob(File.join(output_dir, "**", "index.html")).each do |file|
|
|
62
|
+
path = extract_page_path(file)
|
|
63
|
+
classify_page(File.read(file), path, indexed, excluded)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
[indexed, excluded]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_page_path(file)
|
|
70
|
+
path = file.delete_prefix("#{output_dir}/").delete_suffix("/index.html")
|
|
71
|
+
path.empty? ? "/" : path
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def classify_page(content, path, indexed, excluded)
|
|
75
|
+
if content.include?("data-pagefind-body")
|
|
76
|
+
indexed << path
|
|
77
|
+
elsif content.include?("data-pagefind-ignore")
|
|
78
|
+
excluded << path
|
|
79
|
+
end
|
|
67
80
|
end
|
|
68
81
|
|
|
69
|
-
def
|
|
70
|
-
|
|
82
|
+
def format_index_details(indexed, excluded)
|
|
83
|
+
details = indexed.sort
|
|
84
|
+
excluded.sort.each { |path| details << "(excluded: #{path})" } if excluded.any?
|
|
85
|
+
details
|
|
71
86
|
end
|
|
72
87
|
end
|
|
73
88
|
end
|
|
@@ -4,7 +4,6 @@ require "fileutils"
|
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "open3"
|
|
6
6
|
require "parallel"
|
|
7
|
-
require "tty-progressbar"
|
|
8
7
|
require_relative "../utils/path_utils"
|
|
9
8
|
|
|
10
9
|
module Docyard
|
|
@@ -14,13 +13,15 @@ module Docyard
|
|
|
14
13
|
|
|
15
14
|
PARALLEL_THRESHOLD = 10
|
|
16
15
|
|
|
17
|
-
attr_reader :docs_path, :config, :temp_dir, :pagefind_path
|
|
16
|
+
attr_reader :docs_path, :config, :temp_dir, :pagefind_path, :page_count, :total_pages
|
|
18
17
|
|
|
19
18
|
def initialize(docs_path:, config:)
|
|
20
19
|
@docs_path = docs_path
|
|
21
20
|
@config = config
|
|
22
21
|
@temp_dir = nil
|
|
23
22
|
@pagefind_path = nil
|
|
23
|
+
@page_count = 0
|
|
24
|
+
@total_pages = 0
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def generate
|
|
@@ -29,10 +30,9 @@ module Docyard
|
|
|
29
30
|
|
|
30
31
|
@temp_dir = Dir.mktmpdir("docyard-search-")
|
|
31
32
|
generate_html_files
|
|
32
|
-
page_count = run_pagefind
|
|
33
|
+
@page_count = run_pagefind
|
|
33
34
|
@pagefind_path = File.join(temp_dir, "_docyard", "pagefind")
|
|
34
35
|
|
|
35
|
-
log_success(page_count)
|
|
36
36
|
pagefind_path
|
|
37
37
|
rescue StandardError => e
|
|
38
38
|
Docyard.logger.warn("Search index generation failed: #{e.message}")
|
|
@@ -58,36 +58,27 @@ module Docyard
|
|
|
58
58
|
markdown_files = Dir.glob(File.join(docs_path, "**", "*.md"))
|
|
59
59
|
markdown_files = filter_excluded_files(markdown_files)
|
|
60
60
|
markdown_files = filter_non_indexable_files(markdown_files)
|
|
61
|
-
|
|
62
|
-
progress = TTY::ProgressBar.new(
|
|
63
|
-
"Indexing search [:bar] :current/:total (:percent)",
|
|
64
|
-
total: markdown_files.size,
|
|
65
|
-
width: 50
|
|
66
|
-
)
|
|
67
|
-
mutex = Mutex.new
|
|
61
|
+
@total_pages = markdown_files.size
|
|
68
62
|
|
|
69
63
|
Logging.start_buffering
|
|
70
64
|
if markdown_files.size >= PARALLEL_THRESHOLD
|
|
71
|
-
generate_files_in_parallel(markdown_files
|
|
65
|
+
generate_files_in_parallel(markdown_files)
|
|
72
66
|
else
|
|
73
|
-
generate_files_sequentially(markdown_files
|
|
67
|
+
generate_files_sequentially(markdown_files)
|
|
74
68
|
end
|
|
75
|
-
Logging.flush_warnings
|
|
76
69
|
end
|
|
77
70
|
|
|
78
|
-
def generate_files_in_parallel(markdown_files
|
|
71
|
+
def generate_files_in_parallel(markdown_files)
|
|
79
72
|
Parallel.each(markdown_files, in_threads: Parallel.processor_count) do |file_path|
|
|
80
73
|
renderer = thread_local_renderer
|
|
81
74
|
generate_html_file(file_path, renderer)
|
|
82
|
-
mutex.synchronize { progress.advance }
|
|
83
75
|
end
|
|
84
76
|
end
|
|
85
77
|
|
|
86
|
-
def generate_files_sequentially(markdown_files
|
|
78
|
+
def generate_files_sequentially(markdown_files)
|
|
87
79
|
renderer = Renderer.new(base_url: "/", config: config)
|
|
88
80
|
markdown_files.each do |file_path|
|
|
89
81
|
generate_html_file(file_path, renderer)
|
|
90
|
-
progress.advance
|
|
91
82
|
end
|
|
92
83
|
end
|
|
93
84
|
|
|
@@ -153,11 +144,6 @@ module Docyard
|
|
|
153
144
|
match = output.match(/Indexed (\d+) page/i)
|
|
154
145
|
match ? match[1].to_i : 0
|
|
155
146
|
end
|
|
156
|
-
|
|
157
|
-
def log_success(page_count)
|
|
158
|
-
Docyard.logger.info("* Search index generated (#{page_count} pages indexed)")
|
|
159
|
-
Docyard.logger.debug("* Temp directory: #{temp_dir}")
|
|
160
|
-
end
|
|
161
147
|
end
|
|
162
148
|
end
|
|
163
149
|
end
|
|
@@ -9,13 +9,15 @@ require_relative "sse_server"
|
|
|
9
9
|
require_relative "file_watcher"
|
|
10
10
|
require_relative "../config"
|
|
11
11
|
require_relative "../navigation/sidebar/cache"
|
|
12
|
+
require_relative "../doctor/config_checker"
|
|
13
|
+
require_relative "../doctor/sidebar_checker"
|
|
12
14
|
|
|
13
15
|
module Docyard
|
|
14
16
|
class DevServer
|
|
15
17
|
DEFAULT_PORT = 4200
|
|
16
18
|
DEFAULT_HOST = "localhost"
|
|
17
19
|
|
|
18
|
-
attr_reader :port, :host, :docs_path, :config, :search_enabled
|
|
20
|
+
attr_reader :port, :host, :docs_path, :config, :search_enabled, :global_diagnostics
|
|
19
21
|
|
|
20
22
|
def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, docs_path: "docs", search: false)
|
|
21
23
|
@port = port
|
|
@@ -28,12 +30,13 @@ module Docyard
|
|
|
28
30
|
@file_watcher = nil
|
|
29
31
|
@launcher = nil
|
|
30
32
|
@sidebar_cache = nil
|
|
33
|
+
@global_diagnostics = []
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
def start
|
|
34
37
|
validate_docs_directory!
|
|
35
38
|
build_sidebar_cache
|
|
36
|
-
|
|
39
|
+
collect_global_diagnostics
|
|
37
40
|
setup_hot_reload
|
|
38
41
|
print_server_info
|
|
39
42
|
run_server
|
|
@@ -51,6 +54,14 @@ module Docyard
|
|
|
51
54
|
@sidebar_cache.build
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
def collect_global_diagnostics
|
|
58
|
+
@global_diagnostics.clear
|
|
59
|
+
@global_diagnostics.concat(Doctor::ConfigChecker.new(@config).check)
|
|
60
|
+
@global_diagnostics.concat(Doctor::SidebarChecker.new(File.expand_path(docs_path)).check)
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
Docyard.logger.warn("Diagnostic collection failed: #{e.message}")
|
|
63
|
+
end
|
|
64
|
+
|
|
54
65
|
def generate_search_index
|
|
55
66
|
@search_indexer = Search::DevIndexer.new(
|
|
56
67
|
docs_path: File.expand_path(docs_path),
|
|
@@ -75,11 +86,22 @@ module Docyard
|
|
|
75
86
|
end
|
|
76
87
|
|
|
77
88
|
def handle_file_change(change_type)
|
|
78
|
-
|
|
89
|
+
if change_type == :full
|
|
90
|
+
reload_config
|
|
91
|
+
invalidate_sidebar_cache
|
|
92
|
+
collect_global_diagnostics
|
|
93
|
+
@sse_server.broadcast("diagnostics", { global: @global_diagnostics.map(&:to_h) })
|
|
94
|
+
end
|
|
79
95
|
log_file_change(change_type)
|
|
80
96
|
@sse_server.broadcast("reload", { type: change_type.to_s })
|
|
81
97
|
end
|
|
82
98
|
|
|
99
|
+
def reload_config
|
|
100
|
+
@config = Config.load
|
|
101
|
+
rescue ConfigError => e
|
|
102
|
+
Docyard.logger.warn("Config reload failed: #{e.message}")
|
|
103
|
+
end
|
|
104
|
+
|
|
83
105
|
def invalidate_sidebar_cache
|
|
84
106
|
@sidebar_cache&.invalidate
|
|
85
107
|
@sidebar_cache&.build
|
|
@@ -88,7 +110,7 @@ module Docyard
|
|
|
88
110
|
def log_file_change(change_type)
|
|
89
111
|
message = case change_type
|
|
90
112
|
when :content then "Content changed, reloading..."
|
|
91
|
-
when :
|
|
113
|
+
when :full then "Config changed, full reload..."
|
|
92
114
|
when :asset then "Asset changed, reloading..."
|
|
93
115
|
else "File changed, reloading..."
|
|
94
116
|
end
|
|
@@ -104,17 +126,36 @@ module Docyard
|
|
|
104
126
|
def validate_docs_directory!
|
|
105
127
|
return if File.directory?(docs_path)
|
|
106
128
|
|
|
107
|
-
abort "Error: #{docs_path}/ directory not found.\n" \
|
|
129
|
+
abort "#{UI.error('Error:')} #{docs_path}/ directory not found.\n" \
|
|
108
130
|
"Run `docyard init` first to create the docs structure."
|
|
109
131
|
end
|
|
110
132
|
|
|
111
|
-
def print_server_info
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
def print_server_info # rubocop:disable Metrics/AbcSize
|
|
134
|
+
search_status = @search_enabled ? UI.green("enabled") : UI.dim("disabled")
|
|
135
|
+
|
|
136
|
+
puts
|
|
137
|
+
puts " #{UI.bold('Docyard')} v#{Docyard::VERSION}"
|
|
138
|
+
puts
|
|
139
|
+
puts " Serving #{docs_path}/"
|
|
140
|
+
puts " #{UI.cyan("http://#{host}:#{port}")}"
|
|
141
|
+
puts
|
|
142
|
+
puts " Hot reload: #{UI.cyan("ws://#{host}:#{sse_port}")}"
|
|
143
|
+
puts " Search: #{search_status}"
|
|
144
|
+
print_indexing_status if @search_enabled
|
|
145
|
+
puts
|
|
146
|
+
puts " #{UI.dim('Press Ctrl+C to stop')}"
|
|
147
|
+
puts
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def print_indexing_status
|
|
151
|
+
print " Indexing: #{UI.dim('in progress')}"
|
|
152
|
+
$stdout.flush
|
|
153
|
+
generate_search_index
|
|
154
|
+
count = @search_indexer&.page_count || 0
|
|
155
|
+
total = @search_indexer&.total_pages || 0
|
|
156
|
+
print "\r Indexing: #{UI.green("done (#{count}/#{total} pages)")}\n"
|
|
157
|
+
$stdout.flush
|
|
158
|
+
Logging.flush_warnings
|
|
118
159
|
end
|
|
119
160
|
|
|
120
161
|
def run_server
|
|
@@ -132,7 +173,8 @@ module Docyard
|
|
|
132
173
|
config: @config,
|
|
133
174
|
pagefind_path: @search_indexer&.pagefind_path,
|
|
134
175
|
sse_port: sse_port,
|
|
135
|
-
sidebar_cache: @sidebar_cache
|
|
176
|
+
sidebar_cache: @sidebar_cache,
|
|
177
|
+
global_diagnostics: @global_diagnostics
|
|
136
178
|
)
|
|
137
179
|
end
|
|
138
180
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../editor_launcher"
|
|
5
|
+
|
|
6
|
+
module Docyard
|
|
7
|
+
class ErrorOverlay
|
|
8
|
+
CATEGORY_LABELS = {
|
|
9
|
+
CONFIG: "Configuration",
|
|
10
|
+
SIDEBAR: "Sidebar",
|
|
11
|
+
CONTENT: "Content",
|
|
12
|
+
COMPONENT: "Component",
|
|
13
|
+
LINK: "Links",
|
|
14
|
+
IMAGE: "Images",
|
|
15
|
+
SYNTAX: "Syntax",
|
|
16
|
+
ORPHAN: "Orphan Pages"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
GLOBAL_CATEGORIES = %i[CONFIG SIDEBAR ORPHAN].freeze
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def render(diagnostics:, current_file:, sse_port:)
|
|
23
|
+
return "" if diagnostics.empty?
|
|
24
|
+
|
|
25
|
+
attrs = build_data_attributes(diagnostics, current_file, sse_port)
|
|
26
|
+
render_overlay_html(attrs)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_data_attributes(diagnostics, current_file, sse_port)
|
|
32
|
+
global_count = diagnostics.count { |d| GLOBAL_CATEGORIES.include?(d.category) }
|
|
33
|
+
page_count = diagnostics.length - global_count
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
diagnostics: escape_json(diagnostics),
|
|
37
|
+
current_file: escape_html(current_file),
|
|
38
|
+
error_count: diagnostics.count(&:error?),
|
|
39
|
+
warning_count: diagnostics.count(&:warning?),
|
|
40
|
+
global_count: global_count,
|
|
41
|
+
page_count: page_count,
|
|
42
|
+
sse_port: sse_port,
|
|
43
|
+
editor_available: EditorLauncher.available?
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render_overlay_html(attrs)
|
|
48
|
+
<<~HTML
|
|
49
|
+
<div id="docyard-error-overlay" class="docyard-error-overlay"
|
|
50
|
+
data-diagnostics='#{attrs[:diagnostics]}'
|
|
51
|
+
data-current-file="#{attrs[:current_file]}"
|
|
52
|
+
data-error-count="#{attrs[:error_count]}"
|
|
53
|
+
data-warning-count="#{attrs[:warning_count]}"
|
|
54
|
+
data-global-count="#{attrs[:global_count]}"
|
|
55
|
+
data-page-count="#{attrs[:page_count]}"
|
|
56
|
+
data-sse-port="#{attrs[:sse_port]}"
|
|
57
|
+
data-editor-available="#{attrs[:editor_available]}">
|
|
58
|
+
</div>
|
|
59
|
+
<link rel="stylesheet" href="/_docyard/error-overlay.css">
|
|
60
|
+
<script src="/_docyard/error-overlay.js"></script>
|
|
61
|
+
HTML
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def escape_json(diagnostics)
|
|
65
|
+
JSON.generate(diagnostics.map(&:to_h)).gsub("'", "'")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def escape_html(str)
|
|
69
|
+
str.to_s.gsub("&", "&").gsub('"', """).gsub("<", "<").gsub(">", ">")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../doctor/content_checker"
|
|
4
|
+
require_relative "../doctor/component_checker"
|
|
5
|
+
require_relative "../doctor/link_checker"
|
|
6
|
+
require_relative "../doctor/image_checker"
|
|
7
|
+
|
|
8
|
+
module Docyard
|
|
9
|
+
class PageDiagnostics
|
|
10
|
+
def initialize(docs_path)
|
|
11
|
+
@docs_path = docs_path
|
|
12
|
+
@content_checker = Doctor::ContentChecker.new(docs_path)
|
|
13
|
+
@component_checker = Doctor::ComponentChecker.new(docs_path)
|
|
14
|
+
@link_checker = Doctor::LinkChecker.new(docs_path)
|
|
15
|
+
@image_checker = Doctor::ImageChecker.new(docs_path)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def check(content, file_path)
|
|
19
|
+
[
|
|
20
|
+
@content_checker.check_file(content, file_path),
|
|
21
|
+
@component_checker.check_file(content, file_path),
|
|
22
|
+
@link_checker.check_file(content, file_path),
|
|
23
|
+
@image_checker.check_file(content, file_path)
|
|
24
|
+
].flatten
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -9,14 +9,17 @@ require_relative "static_file_app"
|
|
|
9
9
|
|
|
10
10
|
module Docyard
|
|
11
11
|
class PreviewServer
|
|
12
|
+
include Utils::UrlHelpers
|
|
13
|
+
|
|
12
14
|
DEFAULT_PORT = 4000
|
|
13
15
|
|
|
14
|
-
attr_reader :port, :output_dir
|
|
16
|
+
attr_reader :port, :output_dir, :base_url
|
|
15
17
|
|
|
16
18
|
def initialize(port: DEFAULT_PORT)
|
|
17
19
|
@port = port
|
|
18
20
|
@config = Config.load
|
|
19
21
|
@output_dir = File.expand_path(@config.build.output)
|
|
22
|
+
@base_url = normalize_base_url(@config.build.base)
|
|
20
23
|
@launcher = nil
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -29,21 +32,30 @@ module Docyard
|
|
|
29
32
|
private
|
|
30
33
|
|
|
31
34
|
def validate_output_directory!
|
|
32
|
-
|
|
35
|
+
unless File.directory?(output_dir)
|
|
36
|
+
abort "#{UI.error('Error:')} #{output_dir}/ directory not found.\n" \
|
|
37
|
+
"Run `docyard build` first to build the site."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return if Dir.glob(File.join(output_dir, "**", "*")).any? { |f| File.file?(f) }
|
|
33
41
|
|
|
34
|
-
abort "Error: #{output_dir}/
|
|
42
|
+
abort "#{UI.error('Error:')} #{output_dir}/ is empty.\n" \
|
|
35
43
|
"Run `docyard build` first to build the site."
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
def print_server_info
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
puts
|
|
48
|
+
puts " #{UI.bold('Docyard')} v#{Docyard::VERSION}"
|
|
49
|
+
puts
|
|
50
|
+
puts " Previewing #{output_dir}/"
|
|
51
|
+
puts " #{UI.cyan("http://localhost:#{port}#{base_url}")}"
|
|
52
|
+
puts
|
|
53
|
+
puts " #{UI.dim('Press Ctrl+C to stop')}"
|
|
54
|
+
puts
|
|
43
55
|
end
|
|
44
56
|
|
|
45
57
|
def run_server
|
|
46
|
-
app = StaticFileApp.new(output_dir)
|
|
58
|
+
app = StaticFileApp.new(output_dir, base_path: base_url)
|
|
47
59
|
puma_config = build_puma_config(app)
|
|
48
60
|
log_writer = Puma::LogWriter.strings
|
|
49
61
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
3
|
require "rack"
|
|
5
4
|
require_relative "../navigation/page_navigation_builder"
|
|
6
5
|
require_relative "../navigation/sidebar_builder"
|
|
@@ -9,20 +8,29 @@ require_relative "../constants"
|
|
|
9
8
|
require_relative "../rendering/template_resolver"
|
|
10
9
|
require_relative "../routing/fallback_resolver"
|
|
11
10
|
require_relative "pagefind_handler"
|
|
11
|
+
require_relative "page_diagnostics"
|
|
12
|
+
require_relative "error_overlay"
|
|
13
|
+
require_relative "../editor_launcher"
|
|
12
14
|
|
|
13
15
|
module Docyard
|
|
14
16
|
class RackApplication
|
|
15
|
-
|
|
17
|
+
OVERLAY_RESET_SCRIPT = "<script>try{sessionStorage.setItem('docyard-error-overlay'," \
|
|
18
|
+
"'{\"dismissed\":false,\"lastTotalCount\":0}')}catch(e){}</script>"
|
|
19
|
+
|
|
20
|
+
def initialize(docs_path:, config: nil, pagefind_path: nil, sse_port: nil, sidebar_cache: nil,
|
|
21
|
+
global_diagnostics: [])
|
|
16
22
|
@docs_path = docs_path
|
|
17
23
|
@config = config
|
|
18
24
|
@sse_port = sse_port
|
|
19
25
|
@dev_mode = !sse_port.nil?
|
|
20
26
|
@sidebar_cache = sidebar_cache
|
|
27
|
+
@global_diagnostics = global_diagnostics
|
|
21
28
|
@router = Router.new(docs_path: docs_path)
|
|
22
29
|
@renderer = Renderer.new(base_url: "/", config: config, dev_mode: @dev_mode,
|
|
23
30
|
sse_port: sse_port)
|
|
24
31
|
@asset_handler = AssetHandler.new(public_dir: config&.public_dir || "docs/public")
|
|
25
32
|
@pagefind_handler = PagefindHandler.new(pagefind_path: pagefind_path, config: config)
|
|
33
|
+
@page_diagnostics = PageDiagnostics.new(docs_path) if @dev_mode
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def call(env)
|
|
@@ -36,6 +44,8 @@ module Docyard
|
|
|
36
44
|
def handle_request(env)
|
|
37
45
|
path = env["PATH_INFO"]
|
|
38
46
|
|
|
47
|
+
return handle_open_in_editor(env) if path == "/__docyard/open-in-editor"
|
|
48
|
+
return serve_overlay_asset(path) if path.start_with?("/_docyard/error-overlay")
|
|
39
49
|
return pagefind_handler.serve(path) if path.start_with?(Constants::PAGEFIND_PREFIX)
|
|
40
50
|
return asset_handler.serve_docyard_assets(path) if path.start_with?(Constants::DOCYARD_ASSETS_PREFIX)
|
|
41
51
|
|
|
@@ -94,7 +104,8 @@ module Docyard
|
|
|
94
104
|
end
|
|
95
105
|
|
|
96
106
|
def render_documentation_page(file_path, current_path)
|
|
97
|
-
|
|
107
|
+
content = File.read(file_path)
|
|
108
|
+
markdown = Markdown.new(content)
|
|
98
109
|
template_resolver = TemplateResolver.new(markdown.frontmatter, @config&.data)
|
|
99
110
|
branding = branding_options
|
|
100
111
|
|
|
@@ -103,6 +114,8 @@ module Docyard
|
|
|
103
114
|
template_options: template_resolver.to_options,
|
|
104
115
|
current_path: current_path)
|
|
105
116
|
|
|
117
|
+
html = inject_error_overlay(html, content, file_path) if dev_mode
|
|
118
|
+
|
|
106
119
|
[Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
|
|
107
120
|
end
|
|
108
121
|
|
|
@@ -156,5 +169,53 @@ module Docyard
|
|
|
156
169
|
user_agent = env["HTTP_USER_AGENT"]&.slice(0, 50)
|
|
157
170
|
user_agent ? "#{method} #{path} - #{user_agent}" : "#{method} #{path}"
|
|
158
171
|
end
|
|
172
|
+
|
|
173
|
+
def inject_error_overlay(html, content, file_path)
|
|
174
|
+
page_diags = @page_diagnostics.check(content, file_path)
|
|
175
|
+
all_diagnostics = @global_diagnostics + page_diags
|
|
176
|
+
return html.sub("</body>", "#{OVERLAY_RESET_SCRIPT}</body>") if all_diagnostics.empty?
|
|
177
|
+
|
|
178
|
+
current_file = file_path.delete_prefix("#{@docs_path}/")
|
|
179
|
+
overlay = ErrorOverlay.render(
|
|
180
|
+
diagnostics: all_diagnostics,
|
|
181
|
+
current_file: current_file,
|
|
182
|
+
sse_port: @sse_port
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
html.sub("</body>", "#{overlay}</body>")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def handle_open_in_editor(env)
|
|
189
|
+
params = Rack::Utils.parse_query(env["QUERY_STRING"])
|
|
190
|
+
file = params["file"]
|
|
191
|
+
line = params["line"]&.to_i || 1
|
|
192
|
+
|
|
193
|
+
return [400, {}, ["Missing file parameter"]] unless file
|
|
194
|
+
return [404, {}, ["No editor detected"]] unless EditorLauncher.available?
|
|
195
|
+
|
|
196
|
+
full_path = File.join(@docs_path, file)
|
|
197
|
+
EditorLauncher.open(full_path, line)
|
|
198
|
+
[200, {}, ["OK"]]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def serve_overlay_asset(path)
|
|
202
|
+
asset_path = resolve_overlay_asset_path(path)
|
|
203
|
+
return [404, {}, ["Not found"]] unless asset_path && File.exist?(asset_path)
|
|
204
|
+
|
|
205
|
+
content_type = path.end_with?(".css") ? "text/css" : "application/javascript"
|
|
206
|
+
[200, { "Content-Type" => content_type }, [File.read(asset_path)]]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def resolve_overlay_asset_path(path)
|
|
210
|
+
asset_name = path.delete_prefix("/_docyard/")
|
|
211
|
+
css_path = File.join(templates_path, "assets", asset_name.sub("error-overlay", "css/error-overlay"))
|
|
212
|
+
return css_path if File.exist?(css_path)
|
|
213
|
+
|
|
214
|
+
File.join(templates_path, "assets", asset_name.sub("error-overlay", "js/error-overlay"))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def templates_path
|
|
218
|
+
File.expand_path("../templates", __dir__)
|
|
219
|
+
end
|
|
159
220
|
end
|
|
160
221
|
end
|
|
@@ -4,15 +4,20 @@ require "rack/mime"
|
|
|
4
4
|
|
|
5
5
|
module Docyard
|
|
6
6
|
class StaticFileApp
|
|
7
|
-
def initialize(root)
|
|
7
|
+
def initialize(root, base_path: "/")
|
|
8
8
|
@root = root
|
|
9
|
+
@base_path = base_path.chomp("/")
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def call(env)
|
|
12
13
|
path = env["PATH_INFO"]
|
|
13
|
-
file_path = File.join(@root, path)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
return serve_not_found unless path_under_base?(path)
|
|
16
|
+
|
|
17
|
+
relative_path = strip_base_path(path)
|
|
18
|
+
file_path = File.join(@root, relative_path)
|
|
19
|
+
|
|
20
|
+
if relative_path.end_with?("/") || relative_path.empty? || File.directory?(file_path)
|
|
16
21
|
index_path = File.join(file_path, "index.html")
|
|
17
22
|
return serve_file(index_path) if File.file?(index_path)
|
|
18
23
|
elsif File.file?(file_path)
|
|
@@ -24,6 +29,18 @@ module Docyard
|
|
|
24
29
|
|
|
25
30
|
private
|
|
26
31
|
|
|
32
|
+
def path_under_base?(path)
|
|
33
|
+
return true if @base_path.empty?
|
|
34
|
+
|
|
35
|
+
path == @base_path || path.start_with?("#{@base_path}/")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def strip_base_path(path)
|
|
39
|
+
return path if @base_path.empty?
|
|
40
|
+
|
|
41
|
+
path.delete_prefix(@base_path)
|
|
42
|
+
end
|
|
43
|
+
|
|
27
44
|
def serve_file(path)
|
|
28
45
|
content = File.read(path)
|
|
29
46
|
content_type = Rack::Mime.mime_type(File.extname(path), "application/octet-stream")
|