rdoc 7.2.0 → 8.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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +3 -4
  3. data/LICENSE.rdoc +4 -0
  4. data/README.md +43 -2
  5. data/doc/markup_reference/markdown.md +104 -3
  6. data/lib/rdoc/code_object/alias.rb +2 -8
  7. data/lib/rdoc/code_object/any_method.rb +11 -6
  8. data/lib/rdoc/code_object/attr.rb +11 -6
  9. data/lib/rdoc/code_object/class_module.rb +62 -32
  10. data/lib/rdoc/code_object/constant.rb +29 -3
  11. data/lib/rdoc/code_object/context/section.rb +4 -35
  12. data/lib/rdoc/code_object/context.rb +39 -34
  13. data/lib/rdoc/code_object/method_attr.rb +9 -15
  14. data/lib/rdoc/code_object/mixin.rb +2 -2
  15. data/lib/rdoc/code_object/top_level.rb +9 -3
  16. data/lib/rdoc/code_object.rb +2 -4
  17. data/lib/rdoc/comment.rb +0 -65
  18. data/lib/rdoc/cross_reference.rb +7 -27
  19. data/lib/rdoc/encoding.rb +3 -3
  20. data/lib/rdoc/generator/aliki.rb +17 -0
  21. data/lib/rdoc/generator/darkfish.rb +12 -6
  22. data/lib/rdoc/generator/json_index.rb +2 -2
  23. data/lib/rdoc/generator/markup.rb +56 -31
  24. data/lib/rdoc/generator/template/aliki/DESIGN.md +536 -0
  25. data/lib/rdoc/generator/template/aliki/_aside_toc.rhtml +1 -1
  26. data/lib/rdoc/generator/template/aliki/_head.rhtml +1 -1
  27. data/lib/rdoc/generator/template/aliki/_sidebar_extends.rhtml +8 -6
  28. data/lib/rdoc/generator/template/aliki/_sidebar_includes.rhtml +8 -6
  29. data/lib/rdoc/generator/template/aliki/_sidebar_installed.rhtml +1 -1
  30. data/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml +2 -2
  31. data/lib/rdoc/generator/template/aliki/_sidebar_sections.rhtml +1 -1
  32. data/lib/rdoc/generator/template/aliki/_sidebar_toggle.rhtml +1 -1
  33. data/lib/rdoc/generator/template/aliki/class.rhtml +56 -46
  34. data/lib/rdoc/generator/template/aliki/css/rdoc.css +337 -111
  35. data/lib/rdoc/generator/template/aliki/index.rhtml +1 -1
  36. data/lib/rdoc/generator/template/aliki/js/aliki.js +20 -18
  37. data/lib/rdoc/generator/template/aliki/page.rhtml +1 -1
  38. data/lib/rdoc/generator/template/aliki/servlet_not_found.rhtml +1 -1
  39. data/lib/rdoc/generator/template/aliki/servlet_root.rhtml +2 -2
  40. data/lib/rdoc/generator/template/darkfish/_sidebar_extends.rhtml +8 -6
  41. data/lib/rdoc/generator/template/darkfish/_sidebar_includes.rhtml +8 -6
  42. data/lib/rdoc/generator/template/darkfish/_sidebar_installed.rhtml +1 -1
  43. data/lib/rdoc/generator/template/darkfish/_sidebar_pages.rhtml +1 -1
  44. data/lib/rdoc/generator/template/darkfish/_sidebar_sections.rhtml +1 -1
  45. data/lib/rdoc/generator/template/darkfish/_sidebar_table_of_contents.rhtml +5 -5
  46. data/lib/rdoc/generator/template/darkfish/class.rhtml +18 -21
  47. data/lib/rdoc/generator/template/darkfish/css/rdoc.css +0 -1
  48. data/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml +3 -3
  49. data/lib/rdoc/i18n/text.rb +3 -3
  50. data/lib/rdoc/markdown.kpeg +15 -10
  51. data/lib/rdoc/markdown.rb +289 -104
  52. data/lib/rdoc/markup/document.rb +2 -2
  53. data/lib/rdoc/markup/formatter.rb +24 -34
  54. data/lib/rdoc/markup/heading.rb +1 -4
  55. data/lib/rdoc/markup/indented_paragraph.rb +1 -1
  56. data/lib/rdoc/markup/list.rb +2 -2
  57. data/lib/rdoc/markup/list_item.rb +2 -2
  58. data/lib/rdoc/markup/pre_process.rb +0 -25
  59. data/lib/rdoc/markup/to_ansi.rb +1 -1
  60. data/lib/rdoc/markup/to_bs.rb +1 -1
  61. data/lib/rdoc/markup/to_html.rb +131 -53
  62. data/lib/rdoc/markup/to_html_crossref.rb +97 -71
  63. data/lib/rdoc/markup/to_html_snippet.rb +5 -5
  64. data/lib/rdoc/markup/to_joined_paragraph.rb +0 -5
  65. data/lib/rdoc/markup/to_label.rb +2 -2
  66. data/lib/rdoc/markup/to_markdown.rb +1 -1
  67. data/lib/rdoc/markup/to_rdoc.rb +2 -2
  68. data/lib/rdoc/markup/to_table_of_contents.rb +1 -1
  69. data/lib/rdoc/markup/to_tt_only.rb +0 -7
  70. data/lib/rdoc/markup/verbatim.rb +1 -1
  71. data/lib/rdoc/options.rb +36 -51
  72. data/lib/rdoc/parser/c.rb +7 -6
  73. data/lib/rdoc/parser/rbs.rb +275 -0
  74. data/lib/rdoc/parser/ruby.rb +954 -2066
  75. data/lib/rdoc/parser/ruby_colorizer.rb +253 -0
  76. data/lib/rdoc/parser.rb +3 -2
  77. data/lib/rdoc/rbs_helper.rb +186 -0
  78. data/lib/rdoc/rdoc.rb +196 -24
  79. data/lib/rdoc/ri/driver.rb +8 -2
  80. data/lib/rdoc/ri/paths.rb +1 -1
  81. data/lib/rdoc/{servlet.rb → ri/servlet.rb} +5 -5
  82. data/lib/rdoc/ri.rb +4 -3
  83. data/lib/rdoc/rubygems_hook.rb +11 -11
  84. data/lib/rdoc/server.rb +460 -0
  85. data/lib/rdoc/stats.rb +147 -124
  86. data/lib/rdoc/store.rb +212 -4
  87. data/lib/rdoc/task.rb +16 -15
  88. data/lib/rdoc/text.rb +1 -118
  89. data/lib/rdoc/token_stream.rb +11 -33
  90. data/lib/rdoc/version.rb +1 -1
  91. data/lib/rdoc.rb +35 -7
  92. data/lib/rubygems_plugin.rb +2 -11
  93. data/rdoc-logo.svg +43 -0
  94. data/rdoc.gemspec +6 -4
  95. metadata +35 -18
  96. data/lib/rdoc/code_object/anon_class.rb +0 -10
  97. data/lib/rdoc/code_object/ghost_method.rb +0 -6
  98. data/lib/rdoc/code_object/meta_method.rb +0 -6
  99. data/lib/rdoc/parser/prism_ruby.rb +0 -1112
  100. data/lib/rdoc/parser/ripper_state_lex.rb +0 -302
  101. data/lib/rdoc/parser/ruby_tools.rb +0 -163
@@ -0,0 +1,460 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'json'
5
+ require 'erb'
6
+ require 'set'
7
+ require 'uri'
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
+
21
+ ##
22
+ # Returns a live-reload polling script with the given +last_change_time+
23
+ # embedded so the browser knows the exact timestamp of the content it
24
+ # received. This avoids a race where a change that occurs between page
25
+ # generation and the first poll would be silently skipped.
26
+
27
+ def self.live_reload_script(last_change_time)
28
+ <<~JS
29
+ <script>
30
+ (function() {
31
+ var lastChange = #{last_change_time.to_json};
32
+ setInterval(function() {
33
+ fetch('/__status').then(function(r) { return r.json(); }).then(function(data) {
34
+ if (data.last_change > lastChange) location.reload();
35
+ lastChange = data.last_change;
36
+ }).catch(function() {});
37
+ }, 1000);
38
+ })();
39
+ </script>
40
+ 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
+ end
67
+
68
+ def record_changed(file)
69
+ reload_rbs_signatures_if_needed file
70
+ changed_files << file
71
+ end
72
+
73
+ def record_removed(file)
74
+ reload_rbs_signatures_if_needed file
75
+ removed_files << file
76
+ end
77
+
78
+ def reload_rbs_signatures?
79
+ @reload_rbs_signatures
80
+ end
81
+
82
+ def source_files_changed?
83
+ !changed_files.empty? || !removed_files.empty?
84
+ end
85
+
86
+ private
87
+
88
+ def reload_rbs_signatures_if_needed(file)
89
+ @reload_rbs_signatures = true if @rdoc.auto_discovered_rbs_signature_file?(file)
90
+ end
91
+ end
92
+
93
+ ##
94
+ # Creates a new server.
95
+ #
96
+ # +rdoc+ is the RDoc::RDoc instance that has already parsed the source
97
+ # files.
98
+ # +port+ is the TCP port to listen on.
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
115
+
116
+ ##
117
+ # Starts the server. Blocks until interrupted.
118
+
119
+ def start
120
+ @tcp_server = TCPServer.new('127.0.0.1', @port)
121
+ @running = true
122
+
123
+ @watcher_thread = start_watcher(@rdoc.watch_files)
124
+
125
+ url = "http://localhost:#{@port}"
126
+ $stderr.puts "\nServing documentation at: \e]8;;#{url}\e\\#{url}\e]8;;\e\\"
127
+ $stderr.puts "Press Ctrl+C to stop.\n\n"
128
+
129
+ loop do
130
+ client = @tcp_server.accept
131
+ Thread.new(client) { |c| handle_client(c) }
132
+ 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
+ private
142
+
143
+ def measure
144
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
145
+ yield
146
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(1)
147
+ end
148
+
149
+ def create_generator
150
+ gen = RDoc::Generator::Aliki.new(@store, @options)
151
+ gen.file_output = false
152
+ gen.asset_rel_path = ''
153
+ gen.setup
154
+ gen
155
+ end
156
+
157
+ ##
158
+ # Reads an HTTP request from +client+ and dispatches to the router.
159
+
160
+ def handle_client(client)
161
+ client.binmode
162
+
163
+ return unless IO.select([client], nil, nil, 5)
164
+
165
+ request_line = client.gets("\n")
166
+ return unless request_line
167
+
168
+ method, request_uri, = request_line.split(' ', 3)
169
+ return write_response(client, 400, 'text/plain', 'Bad Request') unless request_uri
170
+
171
+ begin
172
+ path = URI.parse(request_uri).path
173
+ rescue URI::InvalidURIError
174
+ return write_response(client, 400, 'text/plain', 'Bad Request')
175
+ end
176
+
177
+ while (line = client.gets("\n"))
178
+ break if line.strip.empty?
179
+ end
180
+
181
+ unless method == 'GET'
182
+ return write_response(client, 405, 'text/plain', 'Method Not Allowed')
183
+ end
184
+
185
+ if path.start_with?('/__') || %r{\A/(?:css|js)/}.match?(path)
186
+ status, content_type, body = route(path)
187
+ else
188
+ duration_ms = measure do
189
+ status, content_type, body = route(path)
190
+ end
191
+ $stderr.puts "#{status} #{path} (#{duration_ms}ms)"
192
+ end
193
+ write_response(client, status, content_type, body)
194
+ rescue => e
195
+ write_response(client, 500, 'text/html', <<~HTML)
196
+ <!DOCTYPE html>
197
+ <html><body>
198
+ <h1>Internal Server Error</h1>
199
+ <pre>#{ERB::Util.html_escape e.message}\n#{ERB::Util.html_escape e.backtrace.join("\n")}</pre>
200
+ </body></html>
201
+ HTML
202
+ ensure
203
+ client.close rescue nil
204
+ end
205
+
206
+ ##
207
+ # Routes a request path and returns [status, content_type, body].
208
+
209
+ def route(path)
210
+ case path
211
+ when '/__status'
212
+ t = @mutex.synchronize { @last_change_time }
213
+ [200, 'application/json', JSON.generate(last_change: t)]
214
+ when '/js/search_data.js'
215
+ # Search data is dynamically generated, not a static asset
216
+ serve_page(path)
217
+ when %r{\A/(?:css|js)/}
218
+ serve_asset(path)
219
+ else
220
+ serve_page(path)
221
+ end
222
+ end
223
+
224
+ ##
225
+ # Writes an HTTP/1.1 response to +client+.
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
242
+
243
+ ##
244
+ # Serves a static asset (CSS, JS) from the Aliki template directory.
245
+
246
+ def serve_asset(path)
247
+ rel_path = path.delete_prefix("/")
248
+ asset_path = File.join(@generator.template_dir, rel_path)
249
+ real_asset = File.expand_path(asset_path)
250
+
251
+ unless real_asset.start_with?("#{@template_dir}/") && File.file?(real_asset)
252
+ return [404, 'text/plain', "Asset not found: #{rel_path}"]
253
+ end
254
+
255
+ ext = File.extname(rel_path)
256
+ content_type = CONTENT_TYPES[ext] || 'application/octet-stream'
257
+ [200, content_type, File.read(real_asset)]
258
+ end
259
+
260
+ ##
261
+ # Serves an HTML page, rendering from the generator or returning a cached
262
+ # version.
263
+
264
+ def serve_page(path)
265
+ name = path.delete_prefix("/")
266
+ name = 'index.html' if name.empty?
267
+
268
+ html = render_page(name)
269
+
270
+ unless html
271
+ not_found = @generator.generate_servlet_not_found(
272
+ "The page <kbd>#{ERB::Util.html_escape path}</kbd> was not found"
273
+ )
274
+ t = @mutex.synchronize { @last_change_time }
275
+ return [404, 'text/html', inject_live_reload(not_found || '', t)]
276
+ end
277
+
278
+ ext = File.extname(name)
279
+ content_type = CONTENT_TYPES[ext] || 'text/html'
280
+ [200, content_type, html]
281
+ end
282
+
283
+ ##
284
+ # Renders a page through the Aliki generator and caches the result.
285
+
286
+ def render_page(name)
287
+ @mutex.synchronize do
288
+ return @page_cache[name] if @page_cache[name]
289
+
290
+ result = generate_page(name)
291
+ return nil unless result
292
+
293
+ result = inject_live_reload(result, @last_change_time) if name.end_with?('.html')
294
+ @page_cache[name] = result
295
+ end
296
+ end
297
+
298
+ ##
299
+ # Dispatches to the appropriate generator method based on the page name.
300
+
301
+ def generate_page(name)
302
+ case name
303
+ when 'index.html'
304
+ @generator.generate_index
305
+ when 'table_of_contents.html'
306
+ @generator.generate_table_of_contents
307
+ when 'js/search_data.js'
308
+ "var search_data = #{JSON.generate(index: @generator.build_search_index)};"
309
+ else
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)
317
+ end
318
+ end
319
+ end
320
+
321
+ ##
322
+ # Injects the live-reload polling script before +</body>+.
323
+
324
+ def inject_live_reload(html, last_change_time)
325
+ html.sub('</body>', "#{self.class.live_reload_script(last_change_time)}</body>")
326
+ end
327
+
328
+ ##
329
+ # Starts a background thread that polls source file mtimes and triggers
330
+ # re-parsing when changes are detected.
331
+
332
+ def start_watcher(source_files)
333
+ @file_mtimes = file_mtimes_for(source_files)
334
+
335
+ Thread.new do
336
+ while @running
337
+ begin
338
+ sleep 1
339
+ check_for_changes
340
+ rescue => e
341
+ $stderr.puts "RDoc server watcher error: #{e.message}"
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ def file_mtimes_for(files)
348
+ files.each_with_object({}) do |f, h|
349
+ h[f] = RDoc.safe_mtime(f)
350
+ end
351
+ end
352
+
353
+ ##
354
+ # Checks for modified, new, and deleted files. Returns true if any
355
+ # changes were found and processed.
356
+
357
+ def check_for_changes
358
+ changes = FileChanges.new @rdoc
359
+ current_files = current_watch_files
360
+ current_file_set = current_files.to_set
361
+
362
+ @file_mtimes.each_key do |file|
363
+ changes.record_removed file unless current_file_set.include? file
364
+ end
365
+
366
+ current_files.each do |file|
367
+ next unless file_changed? file
368
+
369
+ @file_mtimes[file] = nil unless @file_mtimes.key? file
370
+ changes.record_changed file
371
+ end
372
+
373
+ return false unless changes.source_files_changed?
374
+
375
+ reparse_and_refresh changes
376
+ true
377
+ end
378
+
379
+ ##
380
+ # Re-parses changed files, removes deleted files from the store,
381
+ # refreshes the generator, and invalidates caches.
382
+
383
+ def reparse_and_refresh(changes)
384
+ @mutex.synchronize do
385
+ remove_files changes.removed_files
386
+ reparse_files changes.changed_files
387
+ reload_rbs_signatures if changes.reload_rbs_signatures?
388
+ @store.complete(@options.visibility)
389
+ @store.invalidate_type_name_lookup if changes.source_files_changed?
390
+
391
+ @generator.refresh_store_data
392
+ @page_cache.clear
393
+ @last_change_time = Time.now.to_f
394
+ end
395
+ end
396
+
397
+ def current_watch_files
398
+ file_list = @rdoc.normalized_file_list(
399
+ @options.files.empty? ? [@options.root.to_s] : @options.files,
400
+ true, @options.exclude
401
+ )
402
+ @rdoc.remove_unparseable(file_list).keys | @rdoc.auto_discovered_rbs_signature_files
403
+ end
404
+
405
+ def file_changed?(file)
406
+ return true unless @file_mtimes.key? file
407
+
408
+ old_mtime = @file_mtimes[file]
409
+ return true unless old_mtime
410
+
411
+ current_mtime = RDoc.safe_mtime(file)
412
+ current_mtime && current_mtime > old_mtime
413
+ end
414
+
415
+ def remove_files(files)
416
+ return if files.empty?
417
+
418
+ $stderr.puts "Removed: #{files.join(', ')}"
419
+ files.each do |f|
420
+ @file_mtimes.delete(f)
421
+ relative = @rdoc.relative_path_for(f)
422
+ @store.clear_file_contributions(relative)
423
+ @store.remove_file(relative)
424
+ end
425
+ end
426
+
427
+ def reload_rbs_signatures
428
+ duration_ms = measure do
429
+ @rdoc.load_auto_discovered_rbs_signatures
430
+ @rdoc.record_auto_discovered_rbs_signature_mtimes
431
+ @rdoc.auto_discovered_rbs_signature_files.each do |file|
432
+ @file_mtimes[file] = RDoc.safe_mtime(file)
433
+ end
434
+ end
435
+ $stderr.puts "Reloaded RBS signatures (#{duration_ms}ms)"
436
+ end
437
+
438
+ def reparse_files(files)
439
+ return if files.empty?
440
+
441
+ changed_file_names = []
442
+ duration_ms = measure do
443
+ files.each do |f|
444
+ relative = @rdoc.relative_path_for(f)
445
+ changed_file_names << relative
446
+ begin
447
+ @store.clear_file_contributions(relative, keep_position: true)
448
+ @rdoc.parse_file(f)
449
+ @file_mtimes[f] = RDoc.safe_mtime(f)
450
+ rescue => e
451
+ $stderr.puts "Error parsing #{f}: #{e.message}"
452
+ end
453
+ end
454
+
455
+ @store.cleanup_stale_contributions
456
+ end
457
+ $stderr.puts "Re-parsed #{changed_file_names.join(', ')} (#{duration_ms}ms)"
458
+ end
459
+
460
+ end