docyard 1.0.2 → 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 +27 -1
- data/lib/docyard/build/asset_bundler.rb +7 -33
- 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 +30 -32
- 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/sidebar/local_config_loader.rb +44 -21
- data/lib/docyard/rendering/icon_helpers.rb +1 -3
- 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 +17 -13
- data/lib/docyard/server/rack_application.rb +64 -3
- data/lib/docyard/server/resolution_result.rb +0 -4
- 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 +47 -25
- 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
|
@@ -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,6 +9,8 @@ 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
16
|
attr_reader :port, :output_dir, :base_url
|
|
@@ -30,17 +32,26 @@ module Docyard
|
|
|
30
32
|
private
|
|
31
33
|
|
|
32
34
|
def validate_output_directory!
|
|
33
|
-
|
|
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) }
|
|
34
41
|
|
|
35
|
-
abort "Error: #{output_dir}/
|
|
42
|
+
abort "#{UI.error('Error:')} #{output_dir}/ is empty.\n" \
|
|
36
43
|
"Run `docyard build` first to build the site."
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
def print_server_info
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
44
55
|
end
|
|
45
56
|
|
|
46
57
|
def run_server
|
|
@@ -65,12 +76,5 @@ module Docyard
|
|
|
65
76
|
config.quiet
|
|
66
77
|
end
|
|
67
78
|
end
|
|
68
|
-
|
|
69
|
-
def normalize_base_url(url)
|
|
70
|
-
return "/" if url.nil? || url.empty?
|
|
71
|
-
|
|
72
|
-
url = "/#{url}" unless url.start_with?("/")
|
|
73
|
-
url.end_with?("/") ? url : "#{url}/"
|
|
74
|
-
end
|
|
75
79
|
end
|
|
76
80
|
end
|
|
@@ -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
|