bitclust-core 1.2.6 → 1.6.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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/README.md +65 -0
  4. data/Rakefile +32 -0
  5. data/data/bitclust/catalog/ja_JP.UTF-8 +19 -1
  6. data/data/bitclust/searchpage/index.html +90 -0
  7. data/data/bitclust/template/class +69 -8
  8. data/data/bitclust/template/function +2 -2
  9. data/data/bitclust/template/layout +11 -3
  10. data/data/bitclust/template/method +2 -2
  11. data/data/bitclust/template/search +1 -1
  12. data/data/bitclust/template.epub/class +22 -1
  13. data/data/bitclust/template.epub/function +2 -2
  14. data/data/bitclust/template.epub/layout +1 -3
  15. data/data/bitclust/template.epub/method +2 -2
  16. data/data/bitclust/template.lillia/class +26 -5
  17. data/data/bitclust/template.lillia/layout +1 -3
  18. data/data/bitclust/template.lillia/method +2 -2
  19. data/data/bitclust/template.offline/class +49 -5
  20. data/data/bitclust/template.offline/function +2 -2
  21. data/data/bitclust/template.offline/layout +27 -3
  22. data/data/bitclust/template.offline/method +2 -2
  23. data/lib/bitclust/app.rb +17 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +68 -17
  26. data/lib/bitclust/compat.rb +3 -2
  27. data/lib/bitclust/completion.rb +59 -27
  28. data/lib/bitclust/crossrubyutils.rb +9 -5
  29. data/lib/bitclust/database.rb +29 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +2 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +48 -7
  34. data/lib/bitclust/exception.rb +1 -0
  35. data/lib/bitclust/functiondatabase.rb +14 -1
  36. data/lib/bitclust/functionentry.rb +6 -1
  37. data/lib/bitclust/functionreferenceparser.rb +5 -4
  38. data/lib/bitclust/generators/epub.rb +11 -3
  39. data/lib/bitclust/htmlutils.rb +2 -1
  40. data/lib/bitclust/include_graph.rb +375 -0
  41. data/lib/bitclust/include_pruner.rb +144 -0
  42. data/lib/bitclust/interface.rb +2 -1
  43. data/lib/bitclust/libraryentry.rb +9 -7
  44. data/lib/bitclust/lineinput.rb +8 -5
  45. data/lib/bitclust/link_checker.rb +179 -0
  46. data/lib/bitclust/markdown_bridge.rb +186 -0
  47. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  48. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  49. data/lib/bitclust/markdown_tree.rb +217 -0
  50. data/lib/bitclust/mdcompiler.rb +883 -0
  51. data/lib/bitclust/mdparser.rb +347 -0
  52. data/lib/bitclust/messagecatalog.rb +4 -3
  53. data/lib/bitclust/method_since_calculator.rb +117 -0
  54. data/lib/bitclust/methoddatabase.rb +125 -14
  55. data/lib/bitclust/methodentry.rb +130 -7
  56. data/lib/bitclust/methodid.rb +13 -11
  57. data/lib/bitclust/methodsignature.rb +11 -4
  58. data/lib/bitclust/nameutils.rb +42 -22
  59. data/lib/bitclust/parseutils.rb +1 -0
  60. data/lib/bitclust/preprocessor.rb +70 -32
  61. data/lib/bitclust/progress_bar.rb +1 -0
  62. data/lib/bitclust/rdcompiler.rb +197 -96
  63. data/lib/bitclust/refsdatabase.rb +24 -8
  64. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  65. data/lib/bitclust/requesthandler.rb +22 -16
  66. data/lib/bitclust/ridatabase.rb +11 -4
  67. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  68. data/lib/bitclust/rrdparser.rb +197 -30
  69. data/lib/bitclust/runner.rb +18 -2
  70. data/lib/bitclust/screen.rb +81 -11
  71. data/lib/bitclust/search_index_generator.rb +288 -0
  72. data/lib/bitclust/searcher.rb +72 -22
  73. data/lib/bitclust/server.rb +6 -1
  74. data/lib/bitclust/silent_progress_bar.rb +1 -0
  75. data/lib/bitclust/simplesearcher.rb +25 -12
  76. data/lib/bitclust/subcommand.rb +8 -1
  77. data/lib/bitclust/subcommands/ancestors_command.rb +10 -2
  78. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  79. data/lib/bitclust/subcommands/chm_command.rb +16 -7
  80. data/lib/bitclust/subcommands/classes_command.rb +6 -0
  81. data/lib/bitclust/subcommands/epub_command.rb +1 -0
  82. data/lib/bitclust/subcommands/extract_command.rb +6 -0
  83. data/lib/bitclust/subcommands/htmlfile_command.rb +22 -7
  84. data/lib/bitclust/subcommands/init_command.rb +5 -1
  85. data/lib/bitclust/subcommands/list_command.rb +13 -4
  86. data/lib/bitclust/subcommands/lookup_command.rb +29 -9
  87. data/lib/bitclust/subcommands/methods_command.rb +11 -2
  88. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  89. data/lib/bitclust/subcommands/preproc_command.rb +6 -0
  90. data/lib/bitclust/subcommands/property_command.rb +3 -1
  91. data/lib/bitclust/subcommands/query_command.rb +1 -0
  92. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  93. data/lib/bitclust/subcommands/server_command.rb +16 -10
  94. data/lib/bitclust/subcommands/setup_command.rb +25 -10
  95. data/lib/bitclust/subcommands/statichtml_command.rb +132 -6
  96. data/lib/bitclust/subcommands/update_command.rb +111 -2
  97. data/lib/bitclust/syntax_highlighter.rb +52 -5
  98. data/lib/bitclust/textutils.rb +4 -3
  99. data/lib/bitclust/version.rb +2 -1
  100. data/lib/bitclust/version_badges.rb +90 -0
  101. data/lib/bitclust/whole_file_gate.rb +137 -0
  102. data/lib/bitclust.rb +1 -0
  103. data/test/test_app.rb +269 -0
  104. data/test/test_bitclust.rb +2 -0
  105. data/test/test_capi_converter.rb +91 -0
  106. data/test/test_class_screen.rb +413 -0
  107. data/test/test_copy_doc_md.rb +53 -0
  108. data/test/test_doc_converter.rb +78 -0
  109. data/test/test_entity_splitter.rb +231 -0
  110. data/test/test_entry.rb +207 -2
  111. data/test/test_eol_warning.rb +43 -0
  112. data/test/test_functionreferenceparser.rb +11 -0
  113. data/test/test_include_graph.rb +640 -0
  114. data/test/test_include_pruner.rb +133 -0
  115. data/test/test_link_checker.rb +114 -0
  116. data/test/test_lookup_command.rb +135 -0
  117. data/test/test_markdown_bridge.rb +217 -0
  118. data/test/test_markdown_orchestrator.rb +478 -0
  119. data/test/test_markdown_to_rrd.rb +757 -0
  120. data/test/test_markdown_tree.rb +369 -0
  121. data/test/test_mdcompiler.rb +1276 -0
  122. data/test/test_mdparser.rb +538 -0
  123. data/test/test_method_screen.rb +166 -0
  124. data/test/test_method_since_calculator.rb +290 -0
  125. data/test/test_methoddatabase.rb +16 -0
  126. data/test/test_methodentry.rb +328 -0
  127. data/test/test_methodsignature.rb +3 -1
  128. data/test/test_methodsince_command.rb +168 -0
  129. data/test/test_nameutils.rb +22 -0
  130. data/test/test_preprocessor.rb +215 -0
  131. data/test/test_rdcompiler.rb +429 -27
  132. data/test/test_refsdatabase.rb +32 -0
  133. data/test/test_rrd_to_markdown.rb +968 -0
  134. data/test/test_rrdparser.rb +295 -2
  135. data/test/test_run_ruby_wasm.rb +54 -0
  136. data/test/test_run_worker_prelude.rb +83 -0
  137. data/test/test_runner.rb +51 -0
  138. data/test/test_search_index_generator.rb +513 -0
  139. data/test/test_search_screen.rb +48 -0
  140. data/test/test_searcher.rb +153 -0
  141. data/test/test_searchpage_command.rb +167 -0
  142. data/test/test_simplesearcher.rb +28 -0
  143. data/test/test_statichtml_command.rb +204 -0
  144. data/test/test_syntax_highlighter.rb +60 -0
  145. data/test/test_whole_file_gate.rb +175 -0
  146. data/theme/default/js/NOTICE +42 -0
  147. data/theme/default/js/run-worker.js +111 -0
  148. data/theme/default/js/run.js +350 -0
  149. data/theme/default/js/search_controller.js +134 -0
  150. data/theme/default/js/search_init.js +176 -0
  151. data/theme/default/js/search_navigation.js +110 -0
  152. data/theme/default/js/search_page.js +202 -0
  153. data/theme/default/js/search_ranker.js +244 -0
  154. data/theme/default/script.js +91 -18
  155. data/theme/default/search.css +149 -0
  156. data/theme/default/style.css +193 -21
  157. data/theme/default/syntax-highlight.css +8 -0
  158. data/theme/lillia/style.css +45 -1
  159. metadata +178 -21
  160. data/README +0 -32
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  #
2
3
  # bitclust/rrdparser.rb
3
4
  #
@@ -11,6 +12,7 @@ require 'bitclust/compat'
11
12
  require 'bitclust/preprocessor'
12
13
  require 'bitclust/methodid'
13
14
  require 'bitclust/methoddatabase'
15
+ require 'bitclust/methodsignature'
14
16
  require 'bitclust/lineinput'
15
17
  require 'bitclust/parseutils'
16
18
  require 'bitclust/nameutils'
@@ -32,21 +34,24 @@ module BitClust
32
34
  def RRDParser.parse(s, lib, params = {"version" => "1.9.0"})
33
35
  parser = new(MethodDatabase.dummy(params))
34
36
  if s.respond_to?(:to_io)
37
+ # @type var s: File
35
38
  io = s.to_io
36
39
  elsif s.respond_to?(:to_str)
40
+ # @type var s: String
37
41
  s1 = s.to_str
38
42
  require 'stringio'
39
43
  io = StringIO.new(s1)
40
44
  else
41
45
  io = s
42
46
  end
47
+ # @type var io: File | StringIO
43
48
  l = parser.parse(io, lib, params)
44
49
  return l, parser.db
45
50
  end
46
51
 
47
52
  def RRDParser.split_doc(source)
48
53
  if m = /^=(\[a:.*?\])?( +(.*)|([^=].*))\r?\n/.match(source)
49
- title = $3 || $4
54
+ title = $3 || $4 || raise
50
55
  s = m.post_match
51
56
  return title, s
52
57
  end
@@ -140,7 +145,7 @@ module BitClust
140
145
  unless m
141
146
  parse_error "level-1 header syntax error", line
142
147
  end
143
- return m[1], isconst(m[2], line), isconst(m[3], line)
148
+ return (m[1] || raise), isconst((m[2]), line), isconst((m[3]), line)
144
149
  end
145
150
 
146
151
  def isconst(name, line)
@@ -158,7 +163,7 @@ module BitClust
158
163
  read_extends f
159
164
  read_includes f
160
165
  f.skip_blank_lines
161
- @context.klass.source = f.break(/\A==?[^=]|\A---/).join('').rstrip
166
+ @context.klass&.source = f.break(/\A==?[^=]|\A---/).join('').rstrip
162
167
  read_level2_blocks f
163
168
  end
164
169
 
@@ -176,7 +181,7 @@ module BitClust
176
181
  f.skip_blank_lines
177
182
  read_extends f
178
183
  f.skip_blank_lines
179
- @context.klass.source = f.break(/\A==?[^=]|\A---/).join('').rstrip
184
+ @context.klass&.source = f.break(/\A==?[^=]|\A---/).join('').rstrip
180
185
  @context.visibility = :public
181
186
  @context.type = :singleton_method
182
187
  read_level2_blocks f
@@ -218,9 +223,11 @@ module BitClust
218
223
  f.while_match(/\A==[^=]/) do |line|
219
224
  case line.sub(/\A==/, '').strip
220
225
  when /\A((?:public|private|protected)\s+)?(?:(class|singleton|instance)\s+)?methods?\z/i
221
- @context.visibility = ($1 || 'public').downcase.intern
226
+ # @type var visibility: :public | :private | :protected
227
+ visibility = ($1 || 'public').downcase.strip.intern
228
+ @context.visibility = visibility
222
229
  t = ($2 || 'instance').downcase.sub(/class/, 'singleton')
223
- @context.type = "#{t}_method".intern
230
+ @context.type = _ = "#{t}_method".intern
224
231
  when /\AModule\s+Functions?\z/i
225
232
  @context.module_function
226
233
  when /\AConstants?\z/i
@@ -242,10 +249,10 @@ module BitClust
242
249
 
243
250
  def concat_aliases(chunks)
244
251
  return [] if chunks.empty?
245
- result = [chunks.shift]
252
+ result = [chunks.shift || raise]
246
253
  chunks.each do |chunk|
247
- if result.last.alias?(chunk)
248
- result.last.unify chunk
254
+ if result.last&.alias?(chunk)
255
+ result.last&.unify chunk
249
256
  else
250
257
  result.push chunk
251
258
  end
@@ -255,7 +262,7 @@ module BitClust
255
262
 
256
263
  def read_chunks(f)
257
264
  f.skip_blank_lines
258
- result = []
265
+ result = [] #: Array[Chunk]
259
266
  f.while_match(/\A---/) do |line|
260
267
  f.ungets line
261
268
  result.push read_chunk(f)
@@ -263,14 +270,31 @@ module BitClust
263
270
  result
264
271
  end
265
272
 
273
+ # シグネチャ行に続くメソッド属性行({: ...}。直前のシグネチャ行に束縛)
274
+ METHOD_ATTRIBUTE_LINE_RE = /\A\{:.*\}[ \t]*$/
275
+
266
276
  def read_chunk(f)
267
- header = f.span(/\A---/)
277
+ # シグネチャ行の直後には {: ...} 属性行を置ける。属性行を挟んでも
278
+ # ひとつのチャンク(別名グループ)として読む
279
+ header = [] #: Array[String]
280
+ sig_lines = [] #: Array[String]
281
+ while f.next?
282
+ if /\A---/ =~ f.peek
283
+ line = f.gets or raise
284
+ header.push line
285
+ sig_lines.push line
286
+ elsif !sig_lines.empty? && METHOD_ATTRIBUTE_LINE_RE =~ f.peek
287
+ header.push(f.gets || raise)
288
+ else
289
+ break
290
+ end
291
+ end
268
292
  body = f.break(/\A(?:---|={1,2}[^=])/)
269
293
  src = (header + body).join('')
270
- src.location = header[0].location
271
- sigs = header.map {|line| method_signature(line) }
272
- mainsig = check_chunk_signatures(sigs, header[0])
273
- names = sigs.map {|s| s.name }.uniq.sort
294
+ src.location = sig_lines[0].location
295
+ sigs = sig_lines.map {|line| method_signature(line) }
296
+ mainsig = check_chunk_signatures(sigs, sig_lines[0])
297
+ names = sigs.map {|s| s.name }.compact.uniq.sort
274
298
  Chunk.new(mainsig, names, src)
275
299
  end
276
300
 
@@ -312,9 +336,12 @@ module BitClust
312
336
  def method_signature(line)
313
337
  case
314
338
  when m = SIGNATURE.match(line)
315
- Signature.new(*m.captures)
339
+ klass, typemark_, name = m.captures
340
+ # @type var typemark: NameUtils::typemark
341
+ typemark = _ = typemark_
342
+ Signature.new(klass, typemark, name)
316
343
  when m = GVAR.match(line)
317
- Signature.new(nil, '$', m[1][1..-1])
344
+ Signature.new(nil, '$', (m[1] || raise)[1..-1])
318
345
  else
319
346
  parse_error "wrong method signature", line
320
347
  end
@@ -365,10 +392,13 @@ module BitClust
365
392
 
366
393
  def define_object(name, singleton_object_class, location: nil)
367
394
  singleton_object_class = @db.get_class(singleton_object_class) if singleton_object_class
395
+ # steep:ignore:start
368
396
  register_class :object, name, singleton_object_class, location: location
397
+ # steep:ignore:end
369
398
  end
370
399
 
371
400
  def register_class(type, name, superclass, location: nil)
401
+ name or raise
372
402
  @klass = @db.open_class(name) {|c|
373
403
  c.type = type
374
404
  c.superclass = superclass
@@ -400,30 +430,31 @@ module BitClust
400
430
  end
401
431
 
402
432
  def include(name)
403
- @klass.include @db.get_class(name)
433
+ @klass&.include @db.get_class(name)
404
434
  end
405
435
 
406
436
  def extend(name)
407
- @klass.extend @db.get_class(name)
437
+ @klass&.extend @db.get_class(name)
408
438
  end
409
439
 
410
440
  def dynamic_include(name)
411
- @klass.dynamic_include(@db.get_class(name), @library)
441
+ @klass&.dynamic_include(@db.get_class(name), @library)
412
442
  end
413
443
 
414
444
  def dynamic_extend(name)
415
- @klass.dynamic_extend(@db.get_class(name), @library)
445
+ @klass&.dynamic_extend(@db.get_class(name), @library)
416
446
  end
417
447
 
418
448
  # Add a alias +name+ to the alias list.
419
449
  def alias(name)
450
+ klass = @klass || raise
420
451
  @db.open_class(name) do |c|
421
- c.type = @klass.type
452
+ c.type = klass.type
422
453
  c.library = @library
423
- c.aliasof = @klass
424
- c.source = "Alias of [[c:#{@klass.name}]]\n"
454
+ c.aliasof = klass
455
+ c.source = "Alias of [[c:#{klass.name}]]\n"
425
456
  @library.add_class c
426
- @klass.alias c
457
+ klass.alias c
427
458
  end
428
459
  end
429
460
 
@@ -436,7 +467,7 @@ module BitClust
436
467
  end
437
468
 
438
469
  def special_variable
439
- unless @klass and @klass.name == 'Kernel'
470
+ unless @klass and @klass&.name == 'Kernel'
440
471
  raise "must not happen: type=special_variable but class!=Kernel"
441
472
  end
442
473
  @type = :special_variable
@@ -444,17 +475,30 @@ module BitClust
444
475
 
445
476
  def signature
446
477
  return nil unless @klass
447
- Signature.new(@klass.name, @type ? typename2mark(@type) : nil, nil)
478
+ Signature.new(@klass&.name, @type ? typename2mark(@type || raise) : nil, nil)
448
479
  end
449
480
 
450
481
  def define_method(chunk)
451
482
  id = method_id(chunk)
483
+ attrs = method_attributes(chunk)
452
484
  @db.open_method(id) {|m|
453
485
  m.names = chunk.names.sort
454
- m.kind = chunk.source.match?(/^@undef$/) ? :undefined : @kind
486
+ m.kind = if attrs.flags.include?('undef')
487
+ :undefined
488
+ elsif attrs.flags.include?('nomethod')
489
+ :nomethod
490
+ else
491
+ @kind
492
+ end
493
+ # steep:ignore:start
455
494
  m.visibility = @visibility || :public
495
+ # steep:ignore:end
456
496
  m.source = chunk.source
457
497
  m.source_location = chunk.source.location
498
+ attrs.since_until.each do |name, kv|
499
+ m.fill_since(name, kv['since'] || raise) if kv['since']
500
+ m.fill_until(name, kv['until'] || raise) if kv['until']
501
+ end
458
502
  case @kind
459
503
  when :added, :redefined
460
504
  @library.add_method m
@@ -462,10 +506,133 @@ module BitClust
462
506
  }
463
507
  end
464
508
 
509
+ # 現在サポートするメソッド属性({: ...} 属性行のトークン)。
510
+ # - nomethod/undef: 裸語。kind はエントリ単位でしか持てないので、
511
+ # 別名(複数シグネチャ)のエントリでは全シグネチャに同じ属性が
512
+ # 付いていることを要求する
513
+ # - since="X"/until="X"(bitclust#132 P4): シグネチャ単位で束縛され、
514
+ # そのシグネチャの名前だけに適用される。nomethod/undef と違い、
515
+ # 別名ごとに異なる値を持てる(全シグネチャ一致は要求しない)のが
516
+ # 本来の用途。X は "3.2" のように数字とドットのみ。
517
+ # 空値(since="")は「明示的に不明」= バッジ非表示の指定で、メソッド
518
+ # 自体は昔からあるのにドキュメント追加が遅れ、バージョンラダーからの
519
+ # 自動算出が誤った版を出す場合の抑止に使う(空文字が記録されるため
520
+ # 算出値で上書きされず、表示側も空はバッジを出さない)
521
+ METHOD_ATTRIBUTES = %w[nomethod undef]
522
+ KV_METHOD_ATTRIBUTES = %w[since until]
523
+ KV_METHOD_ATTRIBUTE_VALUE_RE = /\A(?:\d+(?:\.\d+)*)?\z/
524
+
525
+ # md の `### def name ...`/`### module_function def name ...`/
526
+ # `### const name`/`### gvar $name` シグネチャ行を rd 形式
527
+ # (`--- name ...`)へ正規化するための接頭辞(MDParser::SIG_RE と
528
+ # 同じパターン)。属性の紐付け先となるメソッド名を
529
+ # MethodSignature.parse で取り出すために使う
530
+ MD_METHOD_SIG_PREFIX_RE = /\A### (?:module_function def |def |const |gvar )/
531
+
532
+ # method_attributes の返り値。
533
+ # - flags: 裸語トークン(nomethod/undef)の集合。エントリ単位の属性
534
+ # なので全シグネチャで同一であることを method_attributes が保証済み
535
+ # - since_until: シグネチャ単位で束縛された since="X"/until="X" を
536
+ # { メソッド名 => { "since" => "X", "until" => "X" } } の形で保持する
537
+ MethodAttributes = Struct.new(:flags, :since_until)
538
+
539
+ # kramdown Block IAL 風の {: ...} 属性行からメタデータを集める。
540
+ # 属性行は「直前のシグネチャ行のみ」に束縛される(kramdown と同じ解釈)。
541
+ # 裸語トークン(nomethod/undef)は kind がエントリ単位でしか持てないため
542
+ # 全シグネチャで同じであることを要求するが、since=/until= はシグネチャ
543
+ # 単位でそのままの名前に適用するため、この一致要求からは除外する。
544
+ # 本文に入ったら探索を打ち切る(コード例中の {: ...} を誤検出しないため)
545
+ def method_attributes(chunk)
546
+ per_sig = [] #: Array[Array[String]]
547
+ since_until = {} #: Hash[String, Hash[String, String]]
548
+ current_name = nil #: String?
549
+ current_keys = nil #: Hash[String, bool]?
550
+ chunk.source.each_line do |line_|
551
+ line = line_.chomp
552
+ case line
553
+ when /\A---\s/, /\A\#\#\#\s/
554
+ per_sig.push []
555
+ current_name = method_attribute_target_name(line)
556
+ current_keys = {}
557
+ when /\A\{:(.*)\}[ \t]*\z/
558
+ break if per_sig.empty?
559
+ ($1 || raise).strip.split(/\s+/).each do |token|
560
+ if METHOD_ATTRIBUTES.include?(token)
561
+ per_sig.last&.push token
562
+ else
563
+ key, value = parse_kv_method_attribute(token, chunk)
564
+ keys = current_keys or raise
565
+ if keys.key?(key)
566
+ raise ParseError,
567
+ "#{chunk.source.location}: duplicate method attribute #{key.inspect} on the same signature"
568
+ end
569
+ keys[key] = true
570
+ name = current_name or
571
+ raise ParseError,
572
+ "#{chunk.source.location}: cannot determine the signature name to bind method attribute #{token.inspect} to"
573
+ (since_until[name] ||= {})[key] = value
574
+ end
575
+ end
576
+ else
577
+ break
578
+ end
579
+ end
580
+ return MethodAttributes.new([], {}) if per_sig.empty?
581
+ sets = per_sig.map {|a| a.uniq.sort }
582
+ unless sets.uniq.size == 1
583
+ raise ParseError,
584
+ "#{chunk.source.location}: method attributes must be the same on every signature of an entry: #{sets.inspect}"
585
+ end
586
+ # 紐付け先の名前がエントリの names と食い違ったら黙って捨てずに
587
+ # エラーにする(名前導出の規約ずれを CI で検出するための安全網)
588
+ since_until.each_key do |name|
589
+ unless chunk.names.include?(name)
590
+ raise ParseError,
591
+ "#{chunk.source.location}: since/until attribute bound to unknown signature name #{name.inspect} (entry names: #{chunk.names.join(', ')})"
592
+ end
593
+ end
594
+ MethodAttributes.new(sets.first || raise, since_until)
595
+ end
596
+
597
+ # since="X"/until="X" 形式のトークンを解析して [key, value] を返す。
598
+ # 受理する形式以外(キーが不明・値が非引用・値が数字とドット以外を
599
+ # 含む等)はすべて ParseError にする
600
+ def parse_kv_method_attribute(token, chunk)
601
+ m = /\A(\w+)=(.*)\z/.match(token)
602
+ if m
603
+ key = m[1] || raise
604
+ raw_value = m[2] || raise
605
+ quoted = /\A"(.*)"\z/.match(raw_value)
606
+ if quoted && KV_METHOD_ATTRIBUTES.include?(key) && KV_METHOD_ATTRIBUTE_VALUE_RE.match?(quoted[1] || raise)
607
+ return [key, (quoted[1] || raise)]
608
+ end
609
+ end
610
+ raise ParseError,
611
+ "#{chunk.source.location}: invalid method attribute #{token.inspect} " \
612
+ "(supported: #{METHOD_ATTRIBUTES.join(', ')}, " \
613
+ "#{KV_METHOD_ATTRIBUTES.map {|k| %Q(#{k}="X") }.join('/')} where X is digits and dots, e.g. since=\"3.2\", " \
614
+ "or empty for explicitly-unknown, e.g. since=\"\")"
615
+ end
616
+ private :parse_kv_method_attribute
617
+
618
+ # シグネチャ行(rd の `--- ...` / md の `### def ...` 等)から、
619
+ # 属性の紐付け先となるメソッド名を取り出す。
620
+ # 特殊変数のシグネチャ名は "$SAFE" のように $ 付きだが、エントリの
621
+ # names は先頭の $ を除いた形("SAFE"。"$$" なら "$")で格納される
622
+ # (method_signature の Signature.new(nil, '$', name[1..-1]) と同じ規約)
623
+ # ため、since_by_name のキーも names に合わせて $ を1つ剥がす
624
+ def method_attribute_target_name(line)
625
+ normalized = line.sub(MD_METHOD_SIG_PREFIX_RE, '--- ')
626
+ MethodSignature.parse(normalized).name.sub(/\A\$/, '')
627
+ rescue ParseError
628
+ nil
629
+ end
630
+ private :method_attribute_target_name
631
+
465
632
  def method_id(chunk)
466
633
  id = MethodID.new
467
634
  id.library = @library
468
- id.klass = chunk.signature.klass ? @db.get_class(chunk.signature.klass) : @klass
635
+ id.klass = chunk.signature.klass ? @db.get_class(chunk.signature.klass) : (@klass || raise)
469
636
  id.type = chunk.signature.typename || @type
470
637
  id.name = chunk.names.sort.first
471
638
  id
@@ -520,7 +687,7 @@ module BitClust
520
687
  end
521
688
 
522
689
  def typename
523
- typemark2name(@type)
690
+ typemark2name(_ = @type)
524
691
  end
525
692
 
526
693
  def same_type?(other)
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  require 'pathname'
2
3
  require 'optparse'
3
4
 
4
5
  unless Object.const_defined?(:Encoding)
5
- $KCODE = 'UTF-8'
6
+ $KCODE = 'UTF-8' # steep:ignore
6
7
  end
7
8
 
8
9
  def libdir
@@ -49,6 +50,7 @@ Subcommands(for users):
49
50
  list List libraries/classes/methods in database.
50
51
  lookup Lookup a library/class/method from database.
51
52
  search Search classes/methods from database.
53
+ server Run HTTP server to browse the reference manual.
52
54
 
53
55
  Subcommands(for developers):
54
56
  ancestors Compare class/module's ancestors between Ruby and DB.
@@ -59,9 +61,12 @@ Subcommands(for developers):
59
61
  extract Extract method entries from source file.
60
62
  classes Display defined classes for all ruby.
61
63
  methods Display defined methods for all ruby.
64
+ methodsince Fill per-name since/until from a version ladder of DBs.
65
+ checklink Report broken [[c:]]/[[m:]]/[[lib:]]/[[d:]]/[[f:]] links.
62
66
 
63
67
  Subcommands(for packagers):
64
68
  statichtml Generate static HTML files.
69
+ searchpage Generate a static cross-version search page.
65
70
  epub Generate EPUB file.
66
71
  chm Generate static HTML files for CHM.
67
72
 
@@ -95,6 +100,7 @@ Global Options:
95
100
  'setup' => BitClust::Subcommands::SetupCommand.new,
96
101
  'server' => BitClust::Subcommands::ServerCommand.new,
97
102
  'statichtml' => BitClust::Subcommands::StatichtmlCommand.new,
103
+ 'searchpage' => BitClust::Subcommands::SearchpageCommand.new,
98
104
  'htmlfile' => BitClust::Subcommands::HtmlfileCommand.new,
99
105
  'chm' => BitClust::Subcommands::ChmCommand.new,
100
106
  'epub' => BitClust::Subcommands::EPUBCommand.new,
@@ -103,6 +109,8 @@ Global Options:
103
109
  'extract' => BitClust::Subcommands::ExtractCommand.new,
104
110
  'classes' => BitClust::Subcommands::ClassesCommand.new,
105
111
  'methods' => BitClust::Subcommands::MethodsCommand.new,
112
+ 'methodsince' => BitClust::Subcommands::MethodsinceCommand.new,
113
+ 'checklink' => BitClust::Subcommands::ChecklinkCommand.new,
106
114
  }
107
115
  end
108
116
 
@@ -133,6 +141,14 @@ Global Options:
133
141
  @version ||= config[:default_version]
134
142
  @prefix ||= "#{config[:database_prefix]}-#{@version}"
135
143
  end
144
+ # DB 必須のサブコマンドで --database も ~/.bitclust/config も無ければ
145
+ # 案内付きで中断する。needs_database? が false のサブコマンドと、
146
+ # 自前で DB を探す search (Searcher) は対象外
147
+ needs_database = cmd.respond_to?(:needs_database?) && cmd.needs_database?
148
+ if needs_database && !@prefix
149
+ error "no database given. Use --database (-d) option or ~/.bitclust/config"
150
+ end
151
+ # @type var options: Subcommand::options
136
152
  options = {
137
153
  :prefix => @prefix,
138
154
  :capi => @capi
@@ -144,7 +160,7 @@ Global Options:
144
160
  end
145
161
 
146
162
  def load_config
147
- home_directory = Pathname(ENV['HOME'])
163
+ home_directory = Pathname(ENV.fetch('HOME'))
148
164
  config_path = home_directory + ".bitclust/config"
149
165
  if config_path.exist?
150
166
  YAML.load_file(config_path)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  #
2
3
  # bitclust/screen.rb
3
4
  #
@@ -12,6 +13,7 @@ require 'bitclust/methodsignature'
12
13
  require 'bitclust/htmlutils'
13
14
  require 'bitclust/nameutils'
14
15
  require 'bitclust/messagecatalog'
16
+ require 'bitclust/version_badges'
15
17
  require 'erb'
16
18
  require 'json'
17
19
  require 'stringio'
@@ -36,6 +38,7 @@ module BitClust
36
38
  private :default_message_catalog
37
39
 
38
40
  def entry_screen(entry, opt)
41
+ @conf[:target_version] = opt[:target_version]
39
42
  new_screen(Screen.for_entry(entry), entry, opt)
40
43
  end
41
44
 
@@ -140,7 +143,7 @@ module BitClust
140
143
  end
141
144
 
142
145
  def method_url(spec)
143
- cname, tmark, mname = *split_method_spec(spec)
146
+ cname, tmark, mname = split_method_spec(spec)
144
147
  "#{@cgi_url}/method/#{classname2id(cname)}/#{typemark2char(tmark)}/#{encodename_url(mname)}"
145
148
  end
146
149
 
@@ -187,7 +190,7 @@ module BitClust
187
190
  private
188
191
 
189
192
  def preproc(template)
190
- template.gsub(/^\.include ([\w\-]+)/) { load($1) }
193
+ template.gsub(/^\.include ([\w\-]+)/) { load($1 || raise) }
191
194
  end
192
195
  end
193
196
 
@@ -225,7 +228,7 @@ module BitClust
225
228
  <body>
226
229
  <h1>Error</h1>
227
230
  <pre>#{escape_html(@error.message)} (#{escape_html(@error.class.name)})
228
- #{@error.backtrace.map {|s| escape_html(s) }.join("\n")}</pre>
231
+ #{@error.backtrace&.map {|s| escape_html(s) }&.join("\n")}</pre>
229
232
  </body>
230
233
  </html>
231
234
  EndHTML
@@ -263,6 +266,7 @@ module BitClust
263
266
  class TemplateScreen < Screen
264
267
  include Translatable
265
268
  include HTMLUtils
269
+ include VersionBadges
266
270
 
267
271
  def initialize(h)
268
272
  @urlmapper = h[:urlmapper]
@@ -312,6 +316,19 @@ module BitClust
312
316
  %Q(<meta name="description" content="">)
313
317
  end
314
318
 
319
+ # True when the build was told (via statichtml --eol-warning) that the
320
+ # documented Ruby version is no longer maintained.
321
+ def eol_warning?
322
+ !!@conf[:eol_warning]
323
+ end
324
+
325
+ # URL of the ruby.wasm module used by the in-browser RUN button on Ruby
326
+ # sample code (statichtml --run-ruby-wasm). nil disables the feature.
327
+ def run_ruby_wasm_url
328
+ url = @conf[:run_ruby_wasm] #: String?
329
+ url
330
+ end
331
+
315
332
  private
316
333
 
317
334
  def default_encoding
@@ -356,6 +373,12 @@ module BitClust
356
373
  @urlmapper.custom_js_url(js)
357
374
  end
358
375
 
376
+ # Relative path from the current page to the site root, used as the
377
+ # prefix for client-side search result links in statichtml output.
378
+ def html_base
379
+ @urlmapper.respond_to?(:bitclust_html_base) ? @urlmapper.bitclust_html_base : '.'
380
+ end
381
+
359
382
  def favicon_url
360
383
  @urlmapper.favicon_url
361
384
  end
@@ -452,16 +475,55 @@ module BitClust
452
475
  rdcompiler().compile(src)
453
476
  end
454
477
 
478
+ # DB が Markdown ソース(update --markdowntree のネイティブパース)なら
479
+ # MDCompiler(GFM モード)で描画する
480
+ def markdown_source?
481
+ db = @conf[:database]
482
+ db && db.properties['source_format'] == 'markdown'
483
+ end
484
+
455
485
  def rdcompiler
456
- opt = {:catalog => message_catalog()}.merge(@conf)
457
- RDCompiler.new(@urlmapper, @hlevel, opt)
486
+ opt = {:catalog => message_catalog()}.merge(@conf) #: RDCompiler::option
487
+ if markdown_source?
488
+ require 'bitclust/mdcompiler'
489
+ gfm_opt = opt.merge(:gfm => true) #: RDCompiler::option
490
+ MDCompiler.new(@urlmapper, @hlevel, gfm_opt)
491
+ else
492
+ RDCompiler.new(@urlmapper, @hlevel, opt)
493
+ end
494
+ end
495
+
496
+ # bitclust#132 P3: data/bitclust/template/class の署名欄で使う。この
497
+ # テンプレートは compile_method(RDCompiler)を経由せず自前でシグネチャを
498
+ # 描画するので、別名(alias)ごとに自分の since/until バッジを付けた
499
+ # <a><code>...</code></a> を1行ずつ組み立て、<br> で連結する
500
+ def method_row_signature_html(m, sigs)
501
+ sigs.map {|sig|
502
+ %Q(<a href="#{h(method_url(m.spec_string))}"><code>#{h(sig.friendly_string)}</code></a>) +
503
+ row_version_badges(m, sig.name)
504
+ }.join('<br>')
458
505
  end
459
506
 
460
507
  def foreach_method_chunk(src)
508
+ require 'bitclust/mdparser' if markdown_source?
509
+ sig_re = markdown_source? ? MDParser::SIG_RE : /\A---/
461
510
  f = LineInput.for_string(src)
462
511
  while f.next?
463
- sigs = f.span(/\A---/).map {|line| MethodSignature.parse(line.rstrip) }
464
- body = f.break(/\A---/).join.split(/\n\n/, 2).first || ''
512
+ # シグネチャ行の直後の {: ...} メソッド属性行はシグネチャブロックの
513
+ # 一部として読み飛ばす(シグネチャにも本文にも含めない)
514
+ sigs = [] #: Array[MethodSignature]
515
+ while f.next?
516
+ if sig_re =~ f.peek
517
+ line = f.gets or raise
518
+ line = line.sub(sig_re, '--- ') if markdown_source?
519
+ sigs.push MethodSignature.parse(line.rstrip)
520
+ elsif !sigs.empty? && /\A\{:.*\}[ \t]*$/ =~ f.peek
521
+ f.gets
522
+ else
523
+ break
524
+ end
525
+ end
526
+ body = f.break(sig_re).join.split(/\n\n/, 2).first || ''
465
527
  yield sigs, body
466
528
  end
467
529
  end
@@ -540,14 +602,16 @@ module BitClust
540
602
  def draw_tree(cs, &block)
541
603
  return if cs.empty?
542
604
  if cs.first.class?
543
- tree = cs.group_by{|c| c.superclass }
605
+ tree = cs.group_by{|c| c.superclass } # steep:ignore
544
606
  tree.each {|key, list| list.sort_by!{|c| c ? c.name : "" } }
545
607
  roots = tree.keys.select{|c| !c || !cs.include?(c) }
546
- roots.map!{|c| tree[c] }.flatten!
608
+ roots.map!{|c| tree[c] }.flatten! # steep:ignore
547
609
  else
548
610
  tree = {}
549
611
  roots = cs
550
612
  end
613
+ # @type var roots: Array[ClassEntry]
614
+ # @type var tree: Hash[ClassEntry, Array[ClassEntry]]
551
615
  draw_treed_entries(roots, tree, &block)
552
616
  end
553
617
 
@@ -682,8 +746,14 @@ module BitClust
682
746
  end
683
747
 
684
748
  def rdcompiler
685
- h = {:force => true, :catalog => message_catalog() }.merge(@conf)
686
- RDCompiler.new(@urlmapper, @hlevel, h)
749
+ h = {:force => true, :catalog => message_catalog() }.merge(@conf) #: RDCompiler::option
750
+ if markdown_source?
751
+ require 'bitclust/mdcompiler'
752
+ gfm_opt = h.merge(:gfm => true) #: RDCompiler::option
753
+ MDCompiler.new(@urlmapper, @hlevel, gfm_opt)
754
+ else
755
+ RDCompiler.new(@urlmapper, @hlevel, h)
756
+ end
687
757
  end
688
758
 
689
759
  def current_url