rdoc 8.0.0 → 8.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/CONTRIBUTING.md +1 -3
- data/RI.md +75 -75
- data/exe/rdoc +2 -2
- data/lib/rdoc/code_object/alias.rb +71 -69
- data/lib/rdoc/code_object/any_method.rb +305 -303
- data/lib/rdoc/code_object/attr.rb +150 -148
- data/lib/rdoc/code_object/class_module.rb +798 -792
- data/lib/rdoc/code_object/constant.rb +175 -173
- data/lib/rdoc/code_object/context/section.rb +142 -138
- data/lib/rdoc/code_object/context.rb +926 -958
- data/lib/rdoc/code_object/extend.rb +7 -5
- data/lib/rdoc/code_object/include.rb +7 -5
- data/lib/rdoc/code_object/method_attr.rb +326 -319
- data/lib/rdoc/code_object/mixin.rb +97 -95
- data/lib/rdoc/code_object/normal_class.rb +77 -78
- data/lib/rdoc/code_object/normal_module.rb +61 -59
- data/lib/rdoc/code_object/require.rb +23 -39
- data/lib/rdoc/code_object/single_class.rb +21 -19
- data/lib/rdoc/code_object/top_level.rb +212 -219
- data/lib/rdoc/code_object.rb +305 -303
- data/lib/rdoc/comment.rb +275 -273
- data/lib/rdoc/cross_reference.rb +192 -190
- data/lib/rdoc/encoding.rb +105 -103
- data/lib/rdoc/erb_partial.rb +13 -11
- data/lib/rdoc/erbio.rb +29 -27
- data/lib/rdoc/generator/aliki.rb +161 -153
- data/lib/rdoc/generator/darkfish.rb +645 -635
- data/lib/rdoc/generator/json_index.rb +233 -229
- data/lib/rdoc/generator/markup.rb +164 -146
- data/lib/rdoc/generator/pot/message_extractor.rb +57 -51
- data/lib/rdoc/generator/pot/po.rb +52 -51
- data/lib/rdoc/generator/pot/po_entry.rb +138 -132
- data/lib/rdoc/generator/pot.rb +85 -81
- data/lib/rdoc/generator/ri.rb +23 -19
- data/lib/rdoc/generator/template/aliki/DESIGN.md +6 -4
- data/lib/rdoc/generator/template/aliki/_footer.rhtml +1 -1
- data/lib/rdoc/generator/template/aliki/_head.rhtml +10 -10
- data/lib/rdoc/generator/template/aliki/_header.rhtml +29 -44
- data/lib/rdoc/generator/template/aliki/_sidebar_search.rhtml +4 -4
- data/lib/rdoc/generator/template/aliki/css/rdoc.css +207 -178
- data/lib/rdoc/generator/template/aliki/js/aliki.js +60 -84
- data/lib/rdoc/generator/template/darkfish/_footer.rhtml +1 -1
- data/lib/rdoc/generator.rb +48 -46
- data/lib/rdoc/i18n/locale.rb +99 -95
- data/lib/rdoc/i18n/text.rb +109 -105
- data/lib/rdoc/i18n.rb +7 -5
- data/lib/rdoc/markdown/byte_runtime.rb +80 -0
- data/lib/rdoc/markdown.kpeg +15 -11
- data/lib/rdoc/markdown.rb +40 -47
- data/lib/rdoc/markup/block_quote.rb +12 -8
- data/lib/rdoc/markup/document.rb +127 -123
- data/lib/rdoc/markup/formatter.rb +219 -215
- data/lib/rdoc/markup/include.rb +33 -29
- data/lib/rdoc/markup/indented_paragraph.rb +37 -33
- data/lib/rdoc/markup/inline_parser.rb +281 -277
- data/lib/rdoc/markup/list.rb +80 -88
- data/lib/rdoc/markup/list_item.rb +73 -85
- data/lib/rdoc/markup/paragraph.rb +23 -19
- data/lib/rdoc/markup/parser.rb +501 -497
- data/lib/rdoc/markup/pre_process.rb +283 -279
- data/lib/rdoc/markup/raw.rb +2 -2
- data/lib/rdoc/markup/rule.rb +16 -12
- data/lib/rdoc/markup/to_ansi.rb +143 -139
- data/lib/rdoc/markup/to_bs.rb +72 -68
- data/lib/rdoc/markup/to_html.rb +594 -565
- data/lib/rdoc/markup/to_html_crossref.rb +234 -230
- data/lib/rdoc/markup/to_html_snippet.rb +232 -227
- data/lib/rdoc/markup/to_joined_paragraph.rb +36 -32
- data/lib/rdoc/markup/to_label.rb +63 -59
- data/lib/rdoc/markup/to_markdown.rb +212 -208
- data/lib/rdoc/markup/to_rdoc.rb +336 -332
- data/lib/rdoc/markup/to_table_of_contents.rb +66 -62
- data/lib/rdoc/markup/to_test.rb +60 -56
- data/lib/rdoc/markup/to_tt_only.rb +84 -80
- data/lib/rdoc/markup/verbatim.rb +62 -58
- data/lib/rdoc/markup.rb +198 -196
- data/lib/rdoc/options.rb +1063 -1061
- data/lib/rdoc/parser/c.rb +1039 -1037
- data/lib/rdoc/parser/changelog.rb +319 -315
- data/lib/rdoc/parser/markdown.rb +17 -13
- data/lib/rdoc/parser/rbs.rb +239 -235
- data/lib/rdoc/parser/rd.rb +17 -13
- data/lib/rdoc/parser/ruby.rb +1245 -1124
- data/lib/rdoc/parser/ruby_colorizer.rb +263 -213
- data/lib/rdoc/parser/simple.rb +31 -27
- data/lib/rdoc/parser/text.rb +12 -8
- data/lib/rdoc/parser.rb +228 -220
- data/lib/rdoc/rbs_helper.rb +1 -1
- data/lib/rdoc/rd/inline.rb +57 -53
- data/lib/rdoc/rd.rb +90 -88
- data/lib/rdoc/rdoc.rb +500 -491
- data/lib/rdoc/ri/driver.rb +1140 -1135
- data/lib/rdoc/ri/formatter.rb +7 -3
- data/lib/rdoc/ri/paths.rb +140 -136
- data/lib/rdoc/ri/servlet.rb +354 -350
- data/lib/rdoc/ri/store.rb +4 -2
- data/lib/rdoc/ri/task.rb +55 -51
- data/lib/rdoc/ri.rb +14 -12
- data/lib/rdoc/rubygems_hook.rb +183 -181
- data/lib/rdoc/server.rb +349 -347
- data/lib/rdoc/stats/normal.rb +46 -42
- data/lib/rdoc/stats/quiet.rb +39 -35
- data/lib/rdoc/stats/verbose.rb +35 -31
- data/lib/rdoc/stats.rb +365 -363
- data/lib/rdoc/store.rb +888 -902
- data/lib/rdoc/task.rb +260 -256
- data/lib/rdoc/text.rb +135 -133
- data/lib/rdoc/token_stream.rb +101 -93
- data/lib/rdoc/tom_doc.rb +203 -201
- data/lib/rdoc/version.rb +1 -1
- metadata +4 -5
- data/lib/rdoc/markdown/literals.kpeg +0 -21
- data/lib/rdoc/markdown/literals.rb +0 -454
data/lib/rdoc/server.rb
CHANGED
|
@@ -6,26 +6,27 @@ require 'erb'
|
|
|
6
6
|
require 'set'
|
|
7
7
|
require 'uri'
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
# A minimal HTTP server for live-reloading RDoc documentation.
|
|
11
|
-
#
|
|
12
|
-
# Uses Ruby's built-in +TCPServer+ (no external dependencies).
|
|
13
|
-
#
|
|
14
|
-
# Used by <tt>rdoc --server</tt> to let developers preview documentation
|
|
15
|
-
# while editing source files. Parses sources once on startup, watches for
|
|
16
|
-
# file changes, re-parses only the changed files, and auto-refreshes the
|
|
17
|
-
# browser via a simple polling script.
|
|
18
|
-
|
|
19
|
-
class RDoc::Server
|
|
20
|
-
|
|
9
|
+
module RDoc
|
|
21
10
|
##
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
11
|
+
# A minimal HTTP server for live-reloading RDoc documentation.
|
|
12
|
+
#
|
|
13
|
+
# Uses Ruby's built-in +TCPServer+ (no external dependencies).
|
|
14
|
+
#
|
|
15
|
+
# Used by <tt>rdoc --server</tt> to let developers preview documentation
|
|
16
|
+
# while editing source files. Parses sources once on startup, watches for
|
|
17
|
+
# file changes, re-parses only the changed files, and auto-refreshes the
|
|
18
|
+
# browser via a simple polling script.
|
|
19
|
+
|
|
20
|
+
class Server
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
##
|
|
23
|
+
# Returns a live-reload polling script with the given +last_change_time+
|
|
24
|
+
# embedded so the browser knows the exact timestamp of the content it
|
|
25
|
+
# received. This avoids a race where a change that occurs between page
|
|
26
|
+
# generation and the first poll would be silently skipped.
|
|
27
|
+
|
|
28
|
+
def self.live_reload_script(last_change_time)
|
|
29
|
+
<<~JS
|
|
29
30
|
<script>
|
|
30
31
|
(function() {
|
|
31
32
|
var lastChange = #{last_change_time.to_json};
|
|
@@ -38,423 +39,424 @@ class RDoc::Server
|
|
|
38
39
|
})();
|
|
39
40
|
</script>
|
|
40
41
|
JS
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
CONTENT_TYPES = {
|
|
44
|
-
'.html' => 'text/html',
|
|
45
|
-
'.css' => 'text/css',
|
|
46
|
-
'.js' => 'application/javascript',
|
|
47
|
-
'.json' => 'application/json',
|
|
48
|
-
}.freeze
|
|
49
|
-
|
|
50
|
-
STATUS_TEXTS = {
|
|
51
|
-
200 => 'OK',
|
|
52
|
-
400 => 'Bad Request',
|
|
53
|
-
404 => 'Not Found',
|
|
54
|
-
405 => 'Method Not Allowed',
|
|
55
|
-
500 => 'Internal Server Error',
|
|
56
|
-
}.freeze
|
|
57
|
-
|
|
58
|
-
class FileChanges # :nodoc:
|
|
59
|
-
attr_reader :changed_files, :removed_files
|
|
60
|
-
|
|
61
|
-
def initialize(rdoc)
|
|
62
|
-
@rdoc = rdoc
|
|
63
|
-
@changed_files = []
|
|
64
|
-
@removed_files = []
|
|
65
|
-
@reload_rbs_signatures = false
|
|
66
42
|
end
|
|
67
43
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
44
|
+
CONTENT_TYPES = {
|
|
45
|
+
'.html' => 'text/html',
|
|
46
|
+
'.css' => 'text/css',
|
|
47
|
+
'.js' => 'application/javascript',
|
|
48
|
+
'.json' => 'application/json',
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
STATUS_TEXTS = {
|
|
52
|
+
200 => 'OK',
|
|
53
|
+
400 => 'Bad Request',
|
|
54
|
+
404 => 'Not Found',
|
|
55
|
+
405 => 'Method Not Allowed',
|
|
56
|
+
500 => 'Internal Server Error',
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
class FileChanges # :nodoc:
|
|
60
|
+
attr_reader :changed_files, :removed_files
|
|
61
|
+
|
|
62
|
+
def initialize(rdoc)
|
|
63
|
+
@rdoc = rdoc
|
|
64
|
+
@changed_files = []
|
|
65
|
+
@removed_files = []
|
|
66
|
+
@reload_rbs_signatures = false
|
|
67
|
+
end
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
def record_changed(file)
|
|
70
|
+
reload_rbs_signatures_if_needed file
|
|
71
|
+
changed_files << file
|
|
72
|
+
end
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
def record_removed(file)
|
|
75
|
+
reload_rbs_signatures_if_needed file
|
|
76
|
+
removed_files << file
|
|
77
|
+
end
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
def reload_rbs_signatures?
|
|
80
|
+
@reload_rbs_signatures
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def source_files_changed?
|
|
84
|
+
!changed_files.empty? || !removed_files.empty?
|
|
85
|
+
end
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
private
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
def reload_rbs_signatures_if_needed(file)
|
|
90
|
+
@reload_rbs_signatures = true if @rdoc.auto_discovered_rbs_signature_file?(file)
|
|
91
|
+
end
|
|
90
92
|
end
|
|
91
|
-
end
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def initialize(rdoc, port)
|
|
101
|
-
@rdoc = rdoc
|
|
102
|
-
@options = rdoc.options
|
|
103
|
-
@store = rdoc.store
|
|
104
|
-
@port = port
|
|
105
|
-
|
|
106
|
-
# Silence stats output — the server prints its own timing.
|
|
107
|
-
@rdoc.stats.verbosity = 0
|
|
108
|
-
@generator = create_generator
|
|
109
|
-
@template_dir = File.expand_path(@generator.template_dir)
|
|
110
|
-
@page_cache = {}
|
|
111
|
-
@last_change_time = Time.now.to_f
|
|
112
|
-
@mutex = Mutex.new
|
|
113
|
-
@running = false
|
|
114
|
-
end
|
|
94
|
+
##
|
|
95
|
+
# Creates a new server.
|
|
96
|
+
#
|
|
97
|
+
# +rdoc+ is the RDoc::RDoc instance that has already parsed the source
|
|
98
|
+
# files.
|
|
99
|
+
# +port+ is the TCP port to listen on.
|
|
115
100
|
|
|
116
|
-
|
|
117
|
-
|
|
101
|
+
def initialize(rdoc, port)
|
|
102
|
+
@rdoc = rdoc
|
|
103
|
+
@options = rdoc.options
|
|
104
|
+
@store = rdoc.store
|
|
105
|
+
@port = port
|
|
106
|
+
|
|
107
|
+
# Silence stats output — the server prints its own timing.
|
|
108
|
+
@rdoc.stats.verbosity = 0
|
|
109
|
+
@generator = create_generator
|
|
110
|
+
@template_dir = File.expand_path(@generator.template_dir)
|
|
111
|
+
@page_cache = {}
|
|
112
|
+
@last_change_time = Time.now.to_f
|
|
113
|
+
@mutex = Mutex.new
|
|
114
|
+
@running = false
|
|
115
|
+
end
|
|
118
116
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
@running = true
|
|
117
|
+
##
|
|
118
|
+
# Starts the server. Blocks until interrupted.
|
|
122
119
|
|
|
123
|
-
|
|
120
|
+
def start
|
|
121
|
+
@tcp_server = TCPServer.new('127.0.0.1', @port)
|
|
122
|
+
@running = true
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
$stderr.puts "\nServing documentation at: \e]8;;#{url}\e\\#{url}\e]8;;\e\\"
|
|
127
|
-
$stderr.puts "Press Ctrl+C to stop.\n\n"
|
|
124
|
+
@watcher_thread = start_watcher(@rdoc.watch_files)
|
|
128
125
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
url = "http://localhost:#{@port}"
|
|
127
|
+
$stderr.puts "\nServing documentation at: \e]8;;#{url}\e\\#{url}\e]8;;\e\\"
|
|
128
|
+
$stderr.puts "Press Ctrl+C to stop.\n\n"
|
|
129
|
+
|
|
130
|
+
loop do
|
|
131
|
+
client = @tcp_server.accept
|
|
132
|
+
Thread.new(client) { |c| handle_client(c) }
|
|
133
|
+
end
|
|
134
|
+
rescue Interrupt
|
|
135
|
+
# Ctrl+C
|
|
136
|
+
ensure
|
|
137
|
+
@running = false
|
|
138
|
+
@tcp_server&.close
|
|
139
|
+
@watcher_thread&.join(2)
|
|
132
140
|
end
|
|
133
|
-
rescue Interrupt
|
|
134
|
-
# Ctrl+C
|
|
135
|
-
ensure
|
|
136
|
-
@running = false
|
|
137
|
-
@tcp_server&.close
|
|
138
|
-
@watcher_thread&.join(2)
|
|
139
|
-
end
|
|
140
141
|
|
|
141
142
|
private
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
def measure
|
|
145
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
146
|
+
yield
|
|
147
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(1)
|
|
148
|
+
end
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
def create_generator
|
|
151
|
+
gen = Generator::Aliki.new(@store, @options)
|
|
152
|
+
gen.file_output = false
|
|
153
|
+
gen.asset_rel_path = ''
|
|
154
|
+
gen.setup
|
|
155
|
+
gen
|
|
156
|
+
end
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
##
|
|
159
|
+
# Reads an HTTP request from +client+ and dispatches to the router.
|
|
159
160
|
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
def handle_client(client)
|
|
162
|
+
client.binmode
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
return unless IO.select([client], nil, nil, 5)
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
request_line = client.gets("\n")
|
|
167
|
+
return unless request_line
|
|
167
168
|
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
method, request_uri, = request_line.split(' ', 3)
|
|
170
|
+
return write_response(client, 400, 'text/plain', 'Bad Request') unless request_uri
|
|
170
171
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
begin
|
|
173
|
+
path = URI.parse(request_uri).path
|
|
174
|
+
rescue URI::InvalidURIError
|
|
175
|
+
return write_response(client, 400, 'text/plain', 'Bad Request')
|
|
176
|
+
end
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
while (line = client.gets("\n"))
|
|
179
|
+
break if line.strip.empty?
|
|
180
|
+
end
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
unless method == 'GET'
|
|
183
|
+
return write_response(client, 405, 'text/plain', 'Method Not Allowed')
|
|
184
|
+
end
|
|
184
185
|
|
|
185
|
-
|
|
186
|
-
status, content_type, body = route(path)
|
|
187
|
-
else
|
|
188
|
-
duration_ms = measure do
|
|
186
|
+
if path.start_with?('/__') || %r{\A/(?:css|js)/}.match?(path)
|
|
189
187
|
status, content_type, body = route(path)
|
|
188
|
+
else
|
|
189
|
+
duration_ms = measure do
|
|
190
|
+
status, content_type, body = route(path)
|
|
191
|
+
end
|
|
192
|
+
$stderr.puts "#{status} #{path} (#{duration_ms}ms)"
|
|
190
193
|
end
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
rescue => e
|
|
195
|
-
write_response(client, 500, 'text/html', <<~HTML)
|
|
194
|
+
write_response(client, status, content_type, body)
|
|
195
|
+
rescue => e
|
|
196
|
+
write_response(client, 500, 'text/html', <<~HTML)
|
|
196
197
|
<!DOCTYPE html>
|
|
197
198
|
<html><body>
|
|
198
199
|
<h1>Internal Server Error</h1>
|
|
199
200
|
<pre>#{ERB::Util.html_escape e.message}\n#{ERB::Util.html_escape e.backtrace.join("\n")}</pre>
|
|
200
201
|
</body></html>
|
|
201
202
|
HTML
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
ensure
|
|
204
|
+
client.close rescue nil
|
|
205
|
+
end
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
207
|
+
##
|
|
208
|
+
# Routes a request path and returns [status, content_type, body].
|
|
209
|
+
|
|
210
|
+
def route(path)
|
|
211
|
+
case path
|
|
212
|
+
when '/__status'
|
|
213
|
+
t = @mutex.synchronize { @last_change_time }
|
|
214
|
+
[200, 'application/json', JSON.generate(last_change: t)]
|
|
215
|
+
when '/js/search_data.js'
|
|
216
|
+
# Search data is dynamically generated, not a static asset
|
|
217
|
+
serve_page(path)
|
|
218
|
+
when %r{\A/(?:css|js)/}
|
|
219
|
+
serve_asset(path)
|
|
220
|
+
else
|
|
221
|
+
serve_page(path)
|
|
222
|
+
end
|
|
221
223
|
end
|
|
222
|
-
end
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def write_response(client, status, content_type, body)
|
|
228
|
-
body_bytes = body.b
|
|
229
|
-
|
|
230
|
-
header = +"HTTP/1.1 #{status} #{STATUS_TEXTS[status] || 'Unknown'}\r\n"
|
|
231
|
-
header << "Content-Type: #{content_type}\r\n"
|
|
232
|
-
header << "Content-Length: #{body_bytes.bytesize}\r\n"
|
|
233
|
-
header << "Connection: close\r\n"
|
|
234
|
-
header << "\r\n"
|
|
235
|
-
|
|
236
|
-
client.write(header)
|
|
237
|
-
client.write(body_bytes)
|
|
238
|
-
client.flush
|
|
239
|
-
rescue Errno::EPIPE
|
|
240
|
-
# Client disconnected before we finished writing — harmless.
|
|
241
|
-
end
|
|
225
|
+
##
|
|
226
|
+
# Writes an HTTP/1.1 response to +client+.
|
|
242
227
|
|
|
243
|
-
|
|
244
|
-
|
|
228
|
+
def write_response(client, status, content_type, body)
|
|
229
|
+
body_bytes = body.b
|
|
245
230
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
231
|
+
header = +"HTTP/1.1 #{status} #{STATUS_TEXTS[status] || 'Unknown'}\r\n"
|
|
232
|
+
header << "Content-Type: #{content_type}\r\n"
|
|
233
|
+
header << "Content-Length: #{body_bytes.bytesize}\r\n"
|
|
234
|
+
header << "Connection: close\r\n"
|
|
235
|
+
header << "\r\n"
|
|
250
236
|
|
|
251
|
-
|
|
252
|
-
|
|
237
|
+
client.write(header)
|
|
238
|
+
client.write(body_bytes)
|
|
239
|
+
client.flush
|
|
240
|
+
rescue Errno::EPIPE
|
|
241
|
+
# Client disconnected before we finished writing — harmless.
|
|
253
242
|
end
|
|
254
243
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
[200, content_type, File.read(real_asset)]
|
|
258
|
-
end
|
|
244
|
+
##
|
|
245
|
+
# Serves a static asset (CSS, JS) from the Aliki template directory.
|
|
259
246
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
247
|
+
def serve_asset(path)
|
|
248
|
+
rel_path = path.delete_prefix("/")
|
|
249
|
+
asset_path = File.join(@generator.template_dir, rel_path)
|
|
250
|
+
real_asset = File.expand_path(asset_path)
|
|
263
251
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
html = render_page(name)
|
|
252
|
+
unless real_asset.start_with?("#{@template_dir}/") && File.file?(real_asset)
|
|
253
|
+
return [404, 'text/plain', "Asset not found: #{rel_path}"]
|
|
254
|
+
end
|
|
269
255
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
)
|
|
274
|
-
t = @mutex.synchronize { @last_change_time }
|
|
275
|
-
return [404, 'text/html', inject_live_reload(not_found || '', t)]
|
|
256
|
+
ext = File.extname(rel_path)
|
|
257
|
+
content_type = CONTENT_TYPES[ext] || 'application/octet-stream'
|
|
258
|
+
[200, content_type, File.read(real_asset)]
|
|
276
259
|
end
|
|
277
260
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
end
|
|
261
|
+
##
|
|
262
|
+
# Serves an HTML page, rendering from the generator or returning a cached
|
|
263
|
+
# version.
|
|
282
264
|
|
|
283
|
-
|
|
284
|
-
|
|
265
|
+
def serve_page(path)
|
|
266
|
+
name = path.delete_prefix("/")
|
|
267
|
+
name = 'index.html' if name.empty?
|
|
285
268
|
|
|
286
|
-
|
|
287
|
-
@mutex.synchronize do
|
|
288
|
-
return @page_cache[name] if @page_cache[name]
|
|
269
|
+
html = render_page(name)
|
|
289
270
|
|
|
290
|
-
|
|
291
|
-
|
|
271
|
+
unless html
|
|
272
|
+
not_found = @generator.generate_servlet_not_found(
|
|
273
|
+
"The page <kbd>#{ERB::Util.html_escape path}</kbd> was not found"
|
|
274
|
+
)
|
|
275
|
+
t = @mutex.synchronize { @last_change_time }
|
|
276
|
+
return [404, 'text/html', inject_live_reload(not_found || '', t)]
|
|
277
|
+
end
|
|
292
278
|
|
|
293
|
-
|
|
294
|
-
|
|
279
|
+
ext = File.extname(name)
|
|
280
|
+
content_type = CONTENT_TYPES[ext] || 'text/html'
|
|
281
|
+
[200, content_type, html]
|
|
295
282
|
end
|
|
296
|
-
end
|
|
297
283
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
text_name = name.chomp('.html')
|
|
311
|
-
class_name = text_name.gsub('/', '::')
|
|
312
|
-
|
|
313
|
-
if klass = @store.find_class_or_module(class_name)
|
|
314
|
-
@generator.generate_class(klass)
|
|
315
|
-
elsif page = @store.find_text_page(text_name.sub(/_([^_]*)\z/, '.\1'))
|
|
316
|
-
@generator.generate_page(page)
|
|
284
|
+
##
|
|
285
|
+
# Renders a page through the Aliki generator and caches the result.
|
|
286
|
+
|
|
287
|
+
def render_page(name)
|
|
288
|
+
@mutex.synchronize do
|
|
289
|
+
return @page_cache[name] if @page_cache[name]
|
|
290
|
+
|
|
291
|
+
result = generate_page(name)
|
|
292
|
+
return nil unless result
|
|
293
|
+
|
|
294
|
+
result = inject_live_reload(result, @last_change_time) if name.end_with?('.html')
|
|
295
|
+
@page_cache[name] = result
|
|
317
296
|
end
|
|
318
297
|
end
|
|
319
|
-
end
|
|
320
298
|
|
|
321
|
-
|
|
322
|
-
|
|
299
|
+
##
|
|
300
|
+
# Dispatches to the appropriate generator method based on the page name.
|
|
301
|
+
|
|
302
|
+
def generate_page(name)
|
|
303
|
+
case name
|
|
304
|
+
when 'index.html'
|
|
305
|
+
@generator.generate_index
|
|
306
|
+
when 'table_of_contents.html'
|
|
307
|
+
@generator.generate_table_of_contents
|
|
308
|
+
when 'js/search_data.js'
|
|
309
|
+
"var search_data = #{JSON.generate(index: @generator.build_search_index)};"
|
|
310
|
+
else
|
|
311
|
+
text_name = name.chomp('.html')
|
|
312
|
+
class_name = text_name.gsub('/', '::')
|
|
313
|
+
|
|
314
|
+
if klass = @store.find_class_or_module(class_name)
|
|
315
|
+
@generator.generate_class(klass)
|
|
316
|
+
elsif page = @store.find_text_page(text_name.sub(/_([^_]*)\z/, '.\1'))
|
|
317
|
+
@generator.generate_page(page)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
323
321
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
end
|
|
322
|
+
##
|
|
323
|
+
# Injects the live-reload polling script before +</body>+.
|
|
327
324
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
325
|
+
def inject_live_reload(html, last_change_time)
|
|
326
|
+
html.sub('</body>', "#{self.class.live_reload_script(last_change_time)}</body>")
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
##
|
|
330
|
+
# Starts a background thread that polls source file mtimes and triggers
|
|
331
|
+
# re-parsing when changes are detected.
|
|
332
|
+
|
|
333
|
+
def start_watcher(source_files)
|
|
334
|
+
@file_mtimes = file_mtimes_for(source_files)
|
|
335
|
+
|
|
336
|
+
Thread.new do
|
|
337
|
+
while @running
|
|
338
|
+
begin
|
|
339
|
+
sleep 1
|
|
340
|
+
check_for_changes
|
|
341
|
+
rescue => e
|
|
342
|
+
$stderr.puts "RDoc server watcher error: #{e.message}"
|
|
343
|
+
end
|
|
342
344
|
end
|
|
343
345
|
end
|
|
344
346
|
end
|
|
345
|
-
end
|
|
346
347
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
def file_mtimes_for(files)
|
|
349
|
+
files.each_with_object({}) do |f, h|
|
|
350
|
+
h[f] = ::RDoc.safe_mtime(f)
|
|
351
|
+
end
|
|
350
352
|
end
|
|
351
|
-
end
|
|
352
353
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
##
|
|
355
|
+
# Checks for modified, new, and deleted files. Returns true if any
|
|
356
|
+
# changes were found and processed.
|
|
356
357
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
358
|
+
def check_for_changes
|
|
359
|
+
changes = FileChanges.new @rdoc
|
|
360
|
+
current_files = current_watch_files
|
|
361
|
+
current_file_set = current_files.to_set
|
|
361
362
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
@file_mtimes.each_key do |file|
|
|
364
|
+
changes.record_removed file unless current_file_set.include? file
|
|
365
|
+
end
|
|
365
366
|
|
|
366
|
-
|
|
367
|
-
|
|
367
|
+
current_files.each do |file|
|
|
368
|
+
next unless file_changed? file
|
|
368
369
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
@file_mtimes[file] = nil unless @file_mtimes.key? file
|
|
371
|
+
changes.record_changed file
|
|
372
|
+
end
|
|
372
373
|
|
|
373
|
-
|
|
374
|
+
return false unless changes.source_files_changed?
|
|
374
375
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
376
|
+
reparse_and_refresh changes
|
|
377
|
+
true
|
|
378
|
+
end
|
|
378
379
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
380
|
+
##
|
|
381
|
+
# Re-parses changed files, removes deleted files from the store,
|
|
382
|
+
# refreshes the generator, and invalidates caches.
|
|
383
|
+
|
|
384
|
+
def reparse_and_refresh(changes)
|
|
385
|
+
@mutex.synchronize do
|
|
386
|
+
remove_files changes.removed_files
|
|
387
|
+
reparse_files changes.changed_files
|
|
388
|
+
reload_rbs_signatures if changes.reload_rbs_signatures?
|
|
389
|
+
@store.complete(@options.visibility)
|
|
390
|
+
@store.invalidate_type_name_lookup if changes.source_files_changed?
|
|
391
|
+
|
|
392
|
+
@generator.refresh_store_data
|
|
393
|
+
@page_cache.clear
|
|
394
|
+
@last_change_time = Time.now.to_f
|
|
395
|
+
end
|
|
394
396
|
end
|
|
395
|
-
end
|
|
396
397
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
398
|
+
def current_watch_files
|
|
399
|
+
file_list = @rdoc.normalized_file_list(
|
|
400
|
+
@options.files.empty? ? [@options.root.to_s] : @options.files,
|
|
401
|
+
true, @options.exclude
|
|
402
|
+
)
|
|
403
|
+
@rdoc.remove_duplicate_files(@rdoc.remove_unparseable(file_list)).keys | @rdoc.auto_discovered_rbs_signature_files
|
|
404
|
+
end
|
|
404
405
|
|
|
405
|
-
|
|
406
|
-
|
|
406
|
+
def file_changed?(file)
|
|
407
|
+
return true unless @file_mtimes.key? file
|
|
407
408
|
|
|
408
|
-
|
|
409
|
-
|
|
409
|
+
old_mtime = @file_mtimes[file]
|
|
410
|
+
return true unless old_mtime
|
|
410
411
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
current_mtime = ::RDoc.safe_mtime(file)
|
|
413
|
+
current_mtime && current_mtime > old_mtime
|
|
414
|
+
end
|
|
414
415
|
|
|
415
|
-
|
|
416
|
-
|
|
416
|
+
def remove_files(files)
|
|
417
|
+
return if files.empty?
|
|
417
418
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
419
|
+
$stderr.puts "Removed: #{files.join(', ')}"
|
|
420
|
+
files.each do |f|
|
|
421
|
+
@file_mtimes.delete(f)
|
|
422
|
+
relative = @rdoc.relative_path_for(f)
|
|
423
|
+
@store.clear_file_contributions(relative)
|
|
424
|
+
@store.remove_file(relative)
|
|
425
|
+
end
|
|
424
426
|
end
|
|
425
|
-
end
|
|
426
427
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
428
|
+
def reload_rbs_signatures
|
|
429
|
+
duration_ms = measure do
|
|
430
|
+
@rdoc.load_auto_discovered_rbs_signatures
|
|
431
|
+
@rdoc.record_auto_discovered_rbs_signature_mtimes
|
|
432
|
+
@rdoc.auto_discovered_rbs_signature_files.each do |file|
|
|
433
|
+
@file_mtimes[file] = ::RDoc.safe_mtime(file)
|
|
434
|
+
end
|
|
433
435
|
end
|
|
436
|
+
$stderr.puts "Reloaded RBS signatures (#{duration_ms}ms)"
|
|
434
437
|
end
|
|
435
|
-
$stderr.puts "Reloaded RBS signatures (#{duration_ms}ms)"
|
|
436
|
-
end
|
|
437
438
|
|
|
438
|
-
|
|
439
|
-
|
|
439
|
+
def reparse_files(files)
|
|
440
|
+
return if files.empty?
|
|
440
441
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
442
|
+
changed_file_names = []
|
|
443
|
+
duration_ms = measure do
|
|
444
|
+
files.each do |f|
|
|
445
|
+
relative = @rdoc.relative_path_for(f)
|
|
446
|
+
changed_file_names << relative
|
|
447
|
+
begin
|
|
448
|
+
@store.clear_file_contributions(relative, keep_position: true)
|
|
449
|
+
@rdoc.parse_file(f)
|
|
450
|
+
@file_mtimes[f] = ::RDoc.safe_mtime(f)
|
|
451
|
+
rescue => e
|
|
452
|
+
$stderr.puts "Error parsing #{f}: #{e.message}"
|
|
453
|
+
end
|
|
452
454
|
end
|
|
453
|
-
end
|
|
454
455
|
|
|
455
|
-
|
|
456
|
+
@store.cleanup_stale_contributions
|
|
457
|
+
end
|
|
458
|
+
$stderr.puts "Re-parsed #{changed_file_names.join(', ')} (#{duration_ms}ms)"
|
|
456
459
|
end
|
|
457
|
-
$stderr.puts "Re-parsed #{changed_file_names.join(', ')} (#{duration_ms}ms)"
|
|
458
|
-
end
|
|
459
460
|
|
|
461
|
+
end
|
|
460
462
|
end
|