bitclust-core 1.3.0 → 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 (152) 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 +48 -4
  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 +16 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +67 -17
  26. data/lib/bitclust/compat.rb +2 -2
  27. data/lib/bitclust/completion.rb +58 -27
  28. data/lib/bitclust/crossrubyutils.rb +8 -5
  29. data/lib/bitclust/database.rb +28 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +1 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +47 -7
  34. data/lib/bitclust/functiondatabase.rb +13 -1
  35. data/lib/bitclust/functionentry.rb +5 -1
  36. data/lib/bitclust/functionreferenceparser.rb +4 -4
  37. data/lib/bitclust/generators/epub.rb +10 -3
  38. data/lib/bitclust/htmlutils.rb +1 -1
  39. data/lib/bitclust/include_graph.rb +375 -0
  40. data/lib/bitclust/include_pruner.rb +144 -0
  41. data/lib/bitclust/interface.rb +1 -1
  42. data/lib/bitclust/libraryentry.rb +8 -7
  43. data/lib/bitclust/lineinput.rb +7 -5
  44. data/lib/bitclust/link_checker.rb +179 -0
  45. data/lib/bitclust/markdown_bridge.rb +186 -0
  46. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  47. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  48. data/lib/bitclust/markdown_tree.rb +217 -0
  49. data/lib/bitclust/mdcompiler.rb +883 -0
  50. data/lib/bitclust/mdparser.rb +347 -0
  51. data/lib/bitclust/messagecatalog.rb +3 -3
  52. data/lib/bitclust/method_since_calculator.rb +117 -0
  53. data/lib/bitclust/methoddatabase.rb +124 -14
  54. data/lib/bitclust/methodentry.rb +125 -7
  55. data/lib/bitclust/methodid.rb +12 -11
  56. data/lib/bitclust/methodsignature.rb +10 -4
  57. data/lib/bitclust/nameutils.rb +41 -22
  58. data/lib/bitclust/preprocessor.rb +66 -29
  59. data/lib/bitclust/rdcompiler.rb +195 -95
  60. data/lib/bitclust/refsdatabase.rb +23 -8
  61. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  62. data/lib/bitclust/requesthandler.rb +21 -16
  63. data/lib/bitclust/ridatabase.rb +10 -4
  64. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  65. data/lib/bitclust/rrdparser.rb +196 -30
  66. data/lib/bitclust/runner.rb +17 -2
  67. data/lib/bitclust/screen.rb +80 -11
  68. data/lib/bitclust/search_index_generator.rb +288 -0
  69. data/lib/bitclust/searcher.rb +69 -20
  70. data/lib/bitclust/simplesearcher.rb +24 -12
  71. data/lib/bitclust/subcommand.rb +7 -1
  72. data/lib/bitclust/subcommands/ancestors_command.rb +9 -2
  73. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  74. data/lib/bitclust/subcommands/chm_command.rb +15 -7
  75. data/lib/bitclust/subcommands/classes_command.rb +5 -0
  76. data/lib/bitclust/subcommands/extract_command.rb +5 -0
  77. data/lib/bitclust/subcommands/htmlfile_command.rb +21 -7
  78. data/lib/bitclust/subcommands/init_command.rb +4 -1
  79. data/lib/bitclust/subcommands/list_command.rb +12 -4
  80. data/lib/bitclust/subcommands/lookup_command.rb +28 -9
  81. data/lib/bitclust/subcommands/methods_command.rb +10 -2
  82. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  83. data/lib/bitclust/subcommands/preproc_command.rb +5 -0
  84. data/lib/bitclust/subcommands/property_command.rb +2 -1
  85. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  86. data/lib/bitclust/subcommands/server_command.rb +10 -8
  87. data/lib/bitclust/subcommands/setup_command.rb +24 -10
  88. data/lib/bitclust/subcommands/statichtml_command.rb +129 -6
  89. data/lib/bitclust/subcommands/update_command.rb +110 -2
  90. data/lib/bitclust/syntax_highlighter.rb +48 -4
  91. data/lib/bitclust/textutils.rb +3 -3
  92. data/lib/bitclust/version.rb +1 -1
  93. data/lib/bitclust/version_badges.rb +90 -0
  94. data/lib/bitclust/whole_file_gate.rb +137 -0
  95. data/test/test_app.rb +269 -0
  96. data/test/test_bitclust.rb +2 -0
  97. data/test/test_capi_converter.rb +91 -0
  98. data/test/test_class_screen.rb +413 -0
  99. data/test/test_copy_doc_md.rb +53 -0
  100. data/test/test_doc_converter.rb +78 -0
  101. data/test/test_entity_splitter.rb +231 -0
  102. data/test/test_entry.rb +207 -2
  103. data/test/test_eol_warning.rb +43 -0
  104. data/test/test_functionreferenceparser.rb +11 -0
  105. data/test/test_include_graph.rb +640 -0
  106. data/test/test_include_pruner.rb +133 -0
  107. data/test/test_link_checker.rb +114 -0
  108. data/test/test_lookup_command.rb +135 -0
  109. data/test/test_markdown_bridge.rb +217 -0
  110. data/test/test_markdown_orchestrator.rb +478 -0
  111. data/test/test_markdown_to_rrd.rb +757 -0
  112. data/test/test_markdown_tree.rb +369 -0
  113. data/test/test_mdcompiler.rb +1276 -0
  114. data/test/test_mdparser.rb +538 -0
  115. data/test/test_method_screen.rb +166 -0
  116. data/test/test_method_since_calculator.rb +290 -0
  117. data/test/test_methoddatabase.rb +16 -0
  118. data/test/test_methodentry.rb +328 -0
  119. data/test/test_methodsignature.rb +3 -1
  120. data/test/test_methodsince_command.rb +168 -0
  121. data/test/test_nameutils.rb +22 -0
  122. data/test/test_preprocessor.rb +215 -0
  123. data/test/test_rdcompiler.rb +401 -30
  124. data/test/test_refsdatabase.rb +32 -0
  125. data/test/test_rrd_to_markdown.rb +968 -0
  126. data/test/test_rrdparser.rb +295 -2
  127. data/test/test_run_ruby_wasm.rb +54 -0
  128. data/test/test_run_worker_prelude.rb +83 -0
  129. data/test/test_runner.rb +51 -0
  130. data/test/test_search_index_generator.rb +513 -0
  131. data/test/test_search_screen.rb +48 -0
  132. data/test/test_searcher.rb +132 -0
  133. data/test/test_searchpage_command.rb +167 -0
  134. data/test/test_simplesearcher.rb +28 -0
  135. data/test/test_statichtml_command.rb +204 -0
  136. data/test/test_syntax_highlighter.rb +60 -0
  137. data/test/test_whole_file_gate.rb +175 -0
  138. data/theme/default/js/NOTICE +42 -0
  139. data/theme/default/js/run-worker.js +111 -0
  140. data/theme/default/js/run.js +350 -0
  141. data/theme/default/js/search_controller.js +134 -0
  142. data/theme/default/js/search_init.js +176 -0
  143. data/theme/default/js/search_navigation.js +110 -0
  144. data/theme/default/js/search_page.js +202 -0
  145. data/theme/default/js/search_ranker.js +244 -0
  146. data/theme/default/script.js +91 -18
  147. data/theme/default/search.css +149 -0
  148. data/theme/default/style.css +193 -21
  149. data/theme/default/syntax-highlight.css +8 -0
  150. data/theme/lillia/style.css +45 -1
  151. metadata +161 -20
  152. data/README +0 -32
@@ -12,6 +12,7 @@ require 'bitclust/compat'
12
12
  require 'bitclust/preprocessor'
13
13
  require 'bitclust/methodid'
14
14
  require 'bitclust/methoddatabase'
15
+ require 'bitclust/methodsignature'
15
16
  require 'bitclust/lineinput'
16
17
  require 'bitclust/parseutils'
17
18
  require 'bitclust/nameutils'
@@ -33,21 +34,24 @@ module BitClust
33
34
  def RRDParser.parse(s, lib, params = {"version" => "1.9.0"})
34
35
  parser = new(MethodDatabase.dummy(params))
35
36
  if s.respond_to?(:to_io)
37
+ # @type var s: File
36
38
  io = s.to_io
37
39
  elsif s.respond_to?(:to_str)
40
+ # @type var s: String
38
41
  s1 = s.to_str
39
42
  require 'stringio'
40
43
  io = StringIO.new(s1)
41
44
  else
42
45
  io = s
43
46
  end
47
+ # @type var io: File | StringIO
44
48
  l = parser.parse(io, lib, params)
45
49
  return l, parser.db
46
50
  end
47
51
 
48
52
  def RRDParser.split_doc(source)
49
53
  if m = /^=(\[a:.*?\])?( +(.*)|([^=].*))\r?\n/.match(source)
50
- title = $3 || $4
54
+ title = $3 || $4 || raise
51
55
  s = m.post_match
52
56
  return title, s
53
57
  end
@@ -141,7 +145,7 @@ module BitClust
141
145
  unless m
142
146
  parse_error "level-1 header syntax error", line
143
147
  end
144
- return m[1], isconst(m[2], line), isconst(m[3], line)
148
+ return (m[1] || raise), isconst((m[2]), line), isconst((m[3]), line)
145
149
  end
146
150
 
147
151
  def isconst(name, line)
@@ -159,7 +163,7 @@ module BitClust
159
163
  read_extends f
160
164
  read_includes f
161
165
  f.skip_blank_lines
162
- @context.klass.source = f.break(/\A==?[^=]|\A---/).join('').rstrip
166
+ @context.klass&.source = f.break(/\A==?[^=]|\A---/).join('').rstrip
163
167
  read_level2_blocks f
164
168
  end
165
169
 
@@ -177,7 +181,7 @@ module BitClust
177
181
  f.skip_blank_lines
178
182
  read_extends f
179
183
  f.skip_blank_lines
180
- @context.klass.source = f.break(/\A==?[^=]|\A---/).join('').rstrip
184
+ @context.klass&.source = f.break(/\A==?[^=]|\A---/).join('').rstrip
181
185
  @context.visibility = :public
182
186
  @context.type = :singleton_method
183
187
  read_level2_blocks f
@@ -219,9 +223,11 @@ module BitClust
219
223
  f.while_match(/\A==[^=]/) do |line|
220
224
  case line.sub(/\A==/, '').strip
221
225
  when /\A((?:public|private|protected)\s+)?(?:(class|singleton|instance)\s+)?methods?\z/i
222
- @context.visibility = ($1 || 'public').downcase.intern
226
+ # @type var visibility: :public | :private | :protected
227
+ visibility = ($1 || 'public').downcase.strip.intern
228
+ @context.visibility = visibility
223
229
  t = ($2 || 'instance').downcase.sub(/class/, 'singleton')
224
- @context.type = "#{t}_method".intern
230
+ @context.type = _ = "#{t}_method".intern
225
231
  when /\AModule\s+Functions?\z/i
226
232
  @context.module_function
227
233
  when /\AConstants?\z/i
@@ -243,10 +249,10 @@ module BitClust
243
249
 
244
250
  def concat_aliases(chunks)
245
251
  return [] if chunks.empty?
246
- result = [chunks.shift]
252
+ result = [chunks.shift || raise]
247
253
  chunks.each do |chunk|
248
- if result.last.alias?(chunk)
249
- result.last.unify chunk
254
+ if result.last&.alias?(chunk)
255
+ result.last&.unify chunk
250
256
  else
251
257
  result.push chunk
252
258
  end
@@ -256,7 +262,7 @@ module BitClust
256
262
 
257
263
  def read_chunks(f)
258
264
  f.skip_blank_lines
259
- result = []
265
+ result = [] #: Array[Chunk]
260
266
  f.while_match(/\A---/) do |line|
261
267
  f.ungets line
262
268
  result.push read_chunk(f)
@@ -264,14 +270,31 @@ module BitClust
264
270
  result
265
271
  end
266
272
 
273
+ # シグネチャ行に続くメソッド属性行({: ...}。直前のシグネチャ行に束縛)
274
+ METHOD_ATTRIBUTE_LINE_RE = /\A\{:.*\}[ \t]*$/
275
+
267
276
  def read_chunk(f)
268
- 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
269
292
  body = f.break(/\A(?:---|={1,2}[^=])/)
270
293
  src = (header + body).join('')
271
- src.location = header[0].location
272
- sigs = header.map {|line| method_signature(line) }
273
- mainsig = check_chunk_signatures(sigs, header[0])
274
- 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
275
298
  Chunk.new(mainsig, names, src)
276
299
  end
277
300
 
@@ -313,9 +336,12 @@ module BitClust
313
336
  def method_signature(line)
314
337
  case
315
338
  when m = SIGNATURE.match(line)
316
- 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)
317
343
  when m = GVAR.match(line)
318
- Signature.new(nil, '$', m[1][1..-1])
344
+ Signature.new(nil, '$', (m[1] || raise)[1..-1])
319
345
  else
320
346
  parse_error "wrong method signature", line
321
347
  end
@@ -366,10 +392,13 @@ module BitClust
366
392
 
367
393
  def define_object(name, singleton_object_class, location: nil)
368
394
  singleton_object_class = @db.get_class(singleton_object_class) if singleton_object_class
395
+ # steep:ignore:start
369
396
  register_class :object, name, singleton_object_class, location: location
397
+ # steep:ignore:end
370
398
  end
371
399
 
372
400
  def register_class(type, name, superclass, location: nil)
401
+ name or raise
373
402
  @klass = @db.open_class(name) {|c|
374
403
  c.type = type
375
404
  c.superclass = superclass
@@ -401,30 +430,31 @@ module BitClust
401
430
  end
402
431
 
403
432
  def include(name)
404
- @klass.include @db.get_class(name)
433
+ @klass&.include @db.get_class(name)
405
434
  end
406
435
 
407
436
  def extend(name)
408
- @klass.extend @db.get_class(name)
437
+ @klass&.extend @db.get_class(name)
409
438
  end
410
439
 
411
440
  def dynamic_include(name)
412
- @klass.dynamic_include(@db.get_class(name), @library)
441
+ @klass&.dynamic_include(@db.get_class(name), @library)
413
442
  end
414
443
 
415
444
  def dynamic_extend(name)
416
- @klass.dynamic_extend(@db.get_class(name), @library)
445
+ @klass&.dynamic_extend(@db.get_class(name), @library)
417
446
  end
418
447
 
419
448
  # Add a alias +name+ to the alias list.
420
449
  def alias(name)
450
+ klass = @klass || raise
421
451
  @db.open_class(name) do |c|
422
- c.type = @klass.type
452
+ c.type = klass.type
423
453
  c.library = @library
424
- c.aliasof = @klass
425
- c.source = "Alias of [[c:#{@klass.name}]]\n"
454
+ c.aliasof = klass
455
+ c.source = "Alias of [[c:#{klass.name}]]\n"
426
456
  @library.add_class c
427
- @klass.alias c
457
+ klass.alias c
428
458
  end
429
459
  end
430
460
 
@@ -437,7 +467,7 @@ module BitClust
437
467
  end
438
468
 
439
469
  def special_variable
440
- unless @klass and @klass.name == 'Kernel'
470
+ unless @klass and @klass&.name == 'Kernel'
441
471
  raise "must not happen: type=special_variable but class!=Kernel"
442
472
  end
443
473
  @type = :special_variable
@@ -445,17 +475,30 @@ module BitClust
445
475
 
446
476
  def signature
447
477
  return nil unless @klass
448
- Signature.new(@klass.name, @type ? typename2mark(@type) : nil, nil)
478
+ Signature.new(@klass&.name, @type ? typename2mark(@type || raise) : nil, nil)
449
479
  end
450
480
 
451
481
  def define_method(chunk)
452
482
  id = method_id(chunk)
483
+ attrs = method_attributes(chunk)
453
484
  @db.open_method(id) {|m|
454
485
  m.names = chunk.names.sort
455
- 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
456
494
  m.visibility = @visibility || :public
495
+ # steep:ignore:end
457
496
  m.source = chunk.source
458
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
459
502
  case @kind
460
503
  when :added, :redefined
461
504
  @library.add_method m
@@ -463,10 +506,133 @@ module BitClust
463
506
  }
464
507
  end
465
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
+
466
632
  def method_id(chunk)
467
633
  id = MethodID.new
468
634
  id.library = @library
469
- 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)
470
636
  id.type = chunk.signature.typename || @type
471
637
  id.name = chunk.names.sort.first
472
638
  id
@@ -521,7 +687,7 @@ module BitClust
521
687
  end
522
688
 
523
689
  def typename
524
- typemark2name(@type)
690
+ typemark2name(_ = @type)
525
691
  end
526
692
 
527
693
  def same_type?(other)
@@ -3,7 +3,7 @@ require 'pathname'
3
3
  require 'optparse'
4
4
 
5
5
  unless Object.const_defined?(:Encoding)
6
- $KCODE = 'UTF-8'
6
+ $KCODE = 'UTF-8' # steep:ignore
7
7
  end
8
8
 
9
9
  def libdir
@@ -50,6 +50,7 @@ Subcommands(for users):
50
50
  list List libraries/classes/methods in database.
51
51
  lookup Lookup a library/class/method from database.
52
52
  search Search classes/methods from database.
53
+ server Run HTTP server to browse the reference manual.
53
54
 
54
55
  Subcommands(for developers):
55
56
  ancestors Compare class/module's ancestors between Ruby and DB.
@@ -60,9 +61,12 @@ Subcommands(for developers):
60
61
  extract Extract method entries from source file.
61
62
  classes Display defined classes for all ruby.
62
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.
63
66
 
64
67
  Subcommands(for packagers):
65
68
  statichtml Generate static HTML files.
69
+ searchpage Generate a static cross-version search page.
66
70
  epub Generate EPUB file.
67
71
  chm Generate static HTML files for CHM.
68
72
 
@@ -96,6 +100,7 @@ Global Options:
96
100
  'setup' => BitClust::Subcommands::SetupCommand.new,
97
101
  'server' => BitClust::Subcommands::ServerCommand.new,
98
102
  'statichtml' => BitClust::Subcommands::StatichtmlCommand.new,
103
+ 'searchpage' => BitClust::Subcommands::SearchpageCommand.new,
99
104
  'htmlfile' => BitClust::Subcommands::HtmlfileCommand.new,
100
105
  'chm' => BitClust::Subcommands::ChmCommand.new,
101
106
  'epub' => BitClust::Subcommands::EPUBCommand.new,
@@ -104,6 +109,8 @@ Global Options:
104
109
  'extract' => BitClust::Subcommands::ExtractCommand.new,
105
110
  'classes' => BitClust::Subcommands::ClassesCommand.new,
106
111
  'methods' => BitClust::Subcommands::MethodsCommand.new,
112
+ 'methodsince' => BitClust::Subcommands::MethodsinceCommand.new,
113
+ 'checklink' => BitClust::Subcommands::ChecklinkCommand.new,
107
114
  }
108
115
  end
109
116
 
@@ -134,6 +141,14 @@ Global Options:
134
141
  @version ||= config[:default_version]
135
142
  @prefix ||= "#{config[:database_prefix]}-#{@version}"
136
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
137
152
  options = {
138
153
  :prefix => @prefix,
139
154
  :capi => @capi
@@ -145,7 +160,7 @@ Global Options:
145
160
  end
146
161
 
147
162
  def load_config
148
- home_directory = Pathname(ENV['HOME'])
163
+ home_directory = Pathname(ENV.fetch('HOME'))
149
164
  config_path = home_directory + ".bitclust/config"
150
165
  if config_path.exist?
151
166
  YAML.load_file(config_path)
@@ -13,6 +13,7 @@ require 'bitclust/methodsignature'
13
13
  require 'bitclust/htmlutils'
14
14
  require 'bitclust/nameutils'
15
15
  require 'bitclust/messagecatalog'
16
+ require 'bitclust/version_badges'
16
17
  require 'erb'
17
18
  require 'json'
18
19
  require 'stringio'
@@ -37,6 +38,7 @@ module BitClust
37
38
  private :default_message_catalog
38
39
 
39
40
  def entry_screen(entry, opt)
41
+ @conf[:target_version] = opt[:target_version]
40
42
  new_screen(Screen.for_entry(entry), entry, opt)
41
43
  end
42
44
 
@@ -141,7 +143,7 @@ module BitClust
141
143
  end
142
144
 
143
145
  def method_url(spec)
144
- cname, tmark, mname = *split_method_spec(spec)
146
+ cname, tmark, mname = split_method_spec(spec)
145
147
  "#{@cgi_url}/method/#{classname2id(cname)}/#{typemark2char(tmark)}/#{encodename_url(mname)}"
146
148
  end
147
149
 
@@ -188,7 +190,7 @@ module BitClust
188
190
  private
189
191
 
190
192
  def preproc(template)
191
- template.gsub(/^\.include ([\w\-]+)/) { load($1) }
193
+ template.gsub(/^\.include ([\w\-]+)/) { load($1 || raise) }
192
194
  end
193
195
  end
194
196
 
@@ -226,7 +228,7 @@ module BitClust
226
228
  <body>
227
229
  <h1>Error</h1>
228
230
  <pre>#{escape_html(@error.message)} (#{escape_html(@error.class.name)})
229
- #{@error.backtrace.map {|s| escape_html(s) }.join("\n")}</pre>
231
+ #{@error.backtrace&.map {|s| escape_html(s) }&.join("\n")}</pre>
230
232
  </body>
231
233
  </html>
232
234
  EndHTML
@@ -264,6 +266,7 @@ module BitClust
264
266
  class TemplateScreen < Screen
265
267
  include Translatable
266
268
  include HTMLUtils
269
+ include VersionBadges
267
270
 
268
271
  def initialize(h)
269
272
  @urlmapper = h[:urlmapper]
@@ -313,6 +316,19 @@ module BitClust
313
316
  %Q(<meta name="description" content="">)
314
317
  end
315
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
+
316
332
  private
317
333
 
318
334
  def default_encoding
@@ -357,6 +373,12 @@ module BitClust
357
373
  @urlmapper.custom_js_url(js)
358
374
  end
359
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
+
360
382
  def favicon_url
361
383
  @urlmapper.favicon_url
362
384
  end
@@ -453,16 +475,55 @@ module BitClust
453
475
  rdcompiler().compile(src)
454
476
  end
455
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
+
456
485
  def rdcompiler
457
- opt = {:catalog => message_catalog()}.merge(@conf)
458
- 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>')
459
505
  end
460
506
 
461
507
  def foreach_method_chunk(src)
508
+ require 'bitclust/mdparser' if markdown_source?
509
+ sig_re = markdown_source? ? MDParser::SIG_RE : /\A---/
462
510
  f = LineInput.for_string(src)
463
511
  while f.next?
464
- sigs = f.span(/\A---/).map {|line| MethodSignature.parse(line.rstrip) }
465
- 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 || ''
466
527
  yield sigs, body
467
528
  end
468
529
  end
@@ -541,14 +602,16 @@ module BitClust
541
602
  def draw_tree(cs, &block)
542
603
  return if cs.empty?
543
604
  if cs.first.class?
544
- tree = cs.group_by{|c| c.superclass }
605
+ tree = cs.group_by{|c| c.superclass } # steep:ignore
545
606
  tree.each {|key, list| list.sort_by!{|c| c ? c.name : "" } }
546
607
  roots = tree.keys.select{|c| !c || !cs.include?(c) }
547
- roots.map!{|c| tree[c] }.flatten!
608
+ roots.map!{|c| tree[c] }.flatten! # steep:ignore
548
609
  else
549
610
  tree = {}
550
611
  roots = cs
551
612
  end
613
+ # @type var roots: Array[ClassEntry]
614
+ # @type var tree: Hash[ClassEntry, Array[ClassEntry]]
552
615
  draw_treed_entries(roots, tree, &block)
553
616
  end
554
617
 
@@ -683,8 +746,14 @@ module BitClust
683
746
  end
684
747
 
685
748
  def rdcompiler
686
- h = {:force => true, :catalog => message_catalog() }.merge(@conf)
687
- 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
688
757
  end
689
758
 
690
759
  def current_url