bitclust-core 1.6.1 → 1.8.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
  4. data/data/bitclust/template/class-index +0 -1
  5. data/data/bitclust/template/function-index +1 -0
  6. data/data/bitclust/template/layout +1 -0
  7. data/data/bitclust/template.epub/class +5 -5
  8. data/data/bitclust/template.epub/class-index +0 -1
  9. data/data/bitclust/template.epub/contents +7 -4
  10. data/data/bitclust/template.epub/layout +4 -4
  11. data/data/bitclust/template.offline/class-index +0 -1
  12. data/data/bitclust/template.offline/function-index +1 -0
  13. data/data/bitclust/template.offline/layout +3 -0
  14. data/lib/bitclust/classentry.rb +1 -1
  15. data/lib/bitclust/docentry.rb +1 -1
  16. data/lib/bitclust/functionentry.rb +1 -1
  17. data/lib/bitclust/generators/epub.rb +69 -5
  18. data/lib/bitclust/irb.rb +74 -0
  19. data/lib/bitclust/markdown_bridge.rb +11 -6
  20. data/lib/bitclust/markdown_exporter.rb +311 -0
  21. data/lib/bitclust/markdown_tree.rb +118 -11
  22. data/lib/bitclust/mdcompiler.rb +15 -1
  23. data/lib/bitclust/mdparser.rb +42 -1
  24. data/lib/bitclust/methoddatabase.rb +17 -4
  25. data/lib/bitclust/methodentry.rb +24 -0
  26. data/lib/bitclust/methodsignature.rb +1 -1
  27. data/lib/bitclust/nameutils.rb +13 -0
  28. data/lib/bitclust/preprocessor.rb +4 -37
  29. data/lib/bitclust/rbs_overload_matcher.rb +179 -0
  30. data/lib/bitclust/rbs_sig_importer.rb +276 -0
  31. data/lib/bitclust/rbs_signatures.rb +125 -0
  32. data/lib/bitclust/rdcompiler.rb +12 -1
  33. data/lib/bitclust/remote.rb +405 -0
  34. data/lib/bitclust/runner.rb +7 -10
  35. data/lib/bitclust/screen.rb +6 -1
  36. data/lib/bitclust/search_index_generator.rb +28 -10
  37. data/lib/bitclust/searcher.rb +54 -25
  38. data/lib/bitclust/subcommands/chm_command.rb +1 -1
  39. data/lib/bitclust/subcommands/epub_command.rb +2 -2
  40. data/lib/bitclust/subcommands/extract_command.rb +7 -0
  41. data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
  42. data/lib/bitclust/subcommands/methods_command.rb +5 -28
  43. data/lib/bitclust/subcommands/query_command.rb +1 -0
  44. data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
  45. data/lib/bitclust/subcommands/server_command.rb +21 -10
  46. data/lib/bitclust/subcommands/setup_command.rb +32 -11
  47. data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
  48. data/lib/bitclust/syntax_highlighter.rb +65 -4
  49. data/lib/bitclust/user_dirs.rb +62 -0
  50. data/lib/bitclust/version.rb +1 -1
  51. data/lib/bitclust-irb.rb +2 -0
  52. data/test/test_chm_command.rb +22 -0
  53. data/test/test_epub_command.rb +60 -0
  54. data/test/test_epub_generator.rb +330 -0
  55. data/test/test_extract_command.rb +45 -0
  56. data/test/test_irb_plugin.rb +212 -0
  57. data/test/test_link_checker.rb +1 -1
  58. data/test/test_markdown_bridge.rb +35 -0
  59. data/test/test_markdown_exporter.rb +260 -0
  60. data/test/test_markdown_tree.rb +144 -0
  61. data/test/test_mdcompiler.rb +47 -5
  62. data/test/test_mdparser.rb +223 -4
  63. data/test/test_methodentry.rb +111 -0
  64. data/test/test_methods_command.rb +32 -0
  65. data/test/test_methodsignature.rb +3 -1
  66. data/test/test_opensearchdescription_screen.rb +39 -0
  67. data/test/test_preprocessor.rb +26 -0
  68. data/test/test_query_command.rb +30 -0
  69. data/test/test_rbs_overload_matcher.rb +170 -0
  70. data/test/test_rbs_sig_importer.rb +237 -0
  71. data/test/test_rbssig_command.rb +186 -0
  72. data/test/test_rdcompiler.rb +99 -1
  73. data/test/test_remote.rb +374 -0
  74. data/test/test_runner.rb +61 -1
  75. data/test/test_search_index_generator.rb +81 -0
  76. data/test/test_searcher.rb +63 -0
  77. data/test/test_server_command.rb +41 -0
  78. data/test/test_setup_command.rb +164 -0
  79. data/test/test_statichtml_command.rb +137 -0
  80. data/test/test_syntax_highlighter.rb +124 -0
  81. data/test/test_user_dirs.rb +62 -0
  82. data/theme/default/js/run.js +14 -0
  83. data/theme/default/js/search_init.js +3 -1
  84. data/theme/default/js/search_page.js +7 -1
  85. data/theme/default/js/version_switcher.js +147 -0
  86. data/theme/default/search.css +21 -0
  87. data/theme/default/style.css +83 -7
  88. data/theme/lillia/style.css +26 -0
  89. metadata +71 -2
  90. data/lib/bitclust/ridatabase.rb +0 -359
@@ -15,6 +15,7 @@ require 'bitclust/textutils'
15
15
  require 'bitclust/messagecatalog'
16
16
  require 'bitclust/syntax_highlighter'
17
17
  require 'bitclust/version_badges'
18
+ require 'bitclust/rbs_signatures'
18
19
  require 'rouge'
19
20
  require 'stringio'
20
21
 
@@ -27,6 +28,7 @@ module BitClust
27
28
  include TextUtils
28
29
  include Translatable
29
30
  include VersionBadges
31
+ include RbsSignatures
30
32
 
31
33
  def initialize(urlmapper, hlevel = 1, opt = {})
32
34
  @urlmapper = urlmapper
@@ -59,7 +61,9 @@ module BitClust
59
61
  @opt = opt
60
62
  @type = :method
61
63
  @method = m
62
- setup(m.source, m) {
64
+ source = m.source
65
+ setup(source, m) {
66
+ prepare_rbs_signatures(m, source)
63
67
  entry
64
68
  }
65
69
  ensure
@@ -71,6 +75,7 @@ module BitClust
71
75
  def setup(src, entry = nil)
72
76
  @f = LineInput.new(StringIO.new(src), entry)
73
77
  @out = StringIO.new
78
+ reset_rbs_signatures
74
79
  yield
75
80
  @out.string
76
81
  end
@@ -135,6 +140,12 @@ module BitClust
135
140
  k, v = line.sub(/\A:/, '').split(':', 2)
136
141
  props[k&.strip] = v&.strip
137
142
  end if @type == :method
143
+ # RBS シグネチャは見出し <dt> 群の直後・説明 <dd> の直前に、この
144
+ # チャンクへ振り分けられたぶんだけ <dt> 行として出す(rbs_sig
145
+ # property が無ければ何も出ない)
146
+ if (rbs_dts = rbs_signature_dts_for_chunk())
147
+ @out.puts rbs_dts
148
+ end
138
149
  @out.puts %Q(<dd class="#{@type.to_s}-description">)
139
150
  undef_message if attrs.include?('undef')
140
151
  while @f.next?
@@ -0,0 +1,405 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ローカル DB が無い環境向けのリモート検索。docs.ruby-lang.org の検索索引
4
+ # (js/search_data.js = SearchIndexGenerator の生成物)を refe と同じ規則で
5
+ # 引いて候補を絞り、1 件に決まれば Markdown 配信(/ja/latest/ 配下の .md =
6
+ # statichtml --markdown-output の生成物)から本文を取得して表示する。
7
+ # 取得したものは UserDirs.cache_home 配下にキャッシュし、1 日以内はそのまま、
8
+ # それより古ければ If-Modified-Since で確認してから使う。接続できなければ
9
+ # 古いキャッシュで動く。名前一覧の表示には searcher.rb の TerminalView を
10
+ # 使う(このファイルは searcher.rb から require される)。
11
+
12
+ require 'json'
13
+ require 'net/http'
14
+ require 'openssl'
15
+ require 'time'
16
+ require 'uri'
17
+ require 'fileutils'
18
+ require 'bitclust/version'
19
+ require 'bitclust/nameutils'
20
+ require 'bitclust/exception'
21
+ require 'bitclust/user_dirs'
22
+
23
+ module BitClust
24
+ class Remote
25
+ include NameUtils
26
+
27
+ DEFAULT_BASE_URL = 'https://docs.ruby-lang.org/ja/latest/'
28
+ SEARCH_URL = 'https://docs.ruby-lang.org/ja/search/'
29
+ INDEX_PATH = 'js/search_data.js'
30
+ ENV_KEY = 'BITCLUST_REMOTE_URL'
31
+
32
+ # 200/304/404 以外の応答(キャッシュも無いとき)
33
+ class HTTPError < StandardError; end
34
+
35
+ NETWORK_ERRORS = [SocketError, IOError, SystemCallError, Timeout::Error,
36
+ OpenSSL::SSL::SSLError, Net::ProtocolError, HTTPError].freeze
37
+
38
+ # BITCLUST_REMOTE_URL にこの値(大小無視)を設定するとリモート検索をしない。
39
+ # 空文字列でも同じだが、Windows のシェル(cmd.exe・PowerShell)では
40
+ # 空の環境変数を作れない(削除になる)ので、無効化の値を別に用意する
41
+ DISABLED_VALUE = 'none'
42
+
43
+ # 既定の取得元。環境変数 BITCLUST_REMOTE_URL があればそれを使い、
44
+ # DISABLED_VALUE か空文字列ならリモート検索をしない(nil)
45
+ def self.default
46
+ url = ENV.fetch(ENV_KEY, DEFAULT_BASE_URL)
47
+ url.empty? || url.casecmp?(DISABLED_VALUE) ? nil : new(base_url: url)
48
+ end
49
+
50
+ attr_reader :base_url
51
+
52
+ # fetcher は (URI, ヘッダの Hash) を受け取って [ステータス, 本文] を返す
53
+ # 呼び出し可能オブジェクト。省略時は Net::HTTP。cache_dir はキャッシュの
54
+ # 置き場所、clock は現在時刻を返す Proc(テスト用)
55
+ def initialize(base_url: DEFAULT_BASE_URL, fetcher: nil,
56
+ cache_dir: UserDirs.cache_home, clock: nil)
57
+ @base_url = base_url.end_with?('/') ? base_url : "#{base_url}/"
58
+ clock ||= -> { Time.now }
59
+ if fetcher
60
+ @http = nil
61
+ @cache = Cache.new(dir: cache_dir.to_s, fetcher: fetcher, clock: clock)
62
+ else
63
+ http = HttpFetcher.new
64
+ @http = http
65
+ @cache = Cache.new(dir: cache_dir.to_s, fetcher: http, clock: clock)
66
+ end
67
+ end
68
+
69
+ # refe と同じ引数 words で検索して結果を io へ書く。何か表示できれば true。
70
+ # 見つからない・取得できない場合はメッセージを書いて false(例外にしない)
71
+ def lookup(words, io, describe_all: false, line: false, class_only: false)
72
+ entries = load_index.search(words, class_only: class_only).sort_by(&:full_name)
73
+ if entries.empty?
74
+ io.puts not_found_message(words, class_only)
75
+ io.puts "検索ページ: #{search_url(words)}" unless words.empty?
76
+ return false
77
+ end
78
+ view = TerminalView.new(Plain.new, { describe_all: describe_all, line: line, encoding: nil }, io: io)
79
+ if line || (entries.size > 1 && !describe_all)
80
+ view.print_names(entries.map(&:full_name))
81
+ else
82
+ entries.each {|e| show_page(e, io) }
83
+ io.puts '(docs.ruby-lang.org から取得しました。手元で検索するには bitclust setup で DB を作成してください)'
84
+ end
85
+ true
86
+ rescue *NETWORK_ERRORS => err
87
+ io.puts "docs.ruby-lang.org に接続できず、キャッシュもありません (#{err.class}: #{err.message})"
88
+ io.puts 'オフラインで使うには bitclust setup で DB を作成してください。'
89
+ false
90
+ ensure
91
+ @http&.close
92
+ end
93
+
94
+ def search_url(words)
95
+ "#{SEARCH_URL}?q=#{URI.encode_www_form_component(words.join(' '))}"
96
+ end
97
+
98
+ # 同じキャッシュファイル(パスと mtime)なら解析済みの索引を使い回す
99
+ def self.index_for(path, body)
100
+ key = [path, File.mtime(path)]
101
+ memo = @index_memo
102
+ return memo[1] if memo && memo[0] == key
103
+ index = Index.parse(body)
104
+ @index_memo = [key, index]
105
+ index
106
+ end
107
+
108
+ private
109
+
110
+ def load_index
111
+ hit = @cache.fetch(URI("#{@base_url}#{INDEX_PATH}"))
112
+ raise HTTPError, "#{INDEX_PATH} not found" unless hit
113
+ self.class.index_for(hit.path, hit.body)
114
+ end
115
+
116
+ def show_page(entry, io)
117
+ html_url = "#{@base_url}#{entry.path}"
118
+ hit = @cache.fetch(URI("#{@base_url}#{entry.path.sub(/\.html\z/, '.md')}"))
119
+ if hit
120
+ io.puts hit.body
121
+ io.puts
122
+ io.puts '---'
123
+ io.puts html_url
124
+ if hit.stale
125
+ io.puts "(オフライン: #{File.mtime(hit.path).strftime('%Y-%m-%d %H:%M')} 取得のキャッシュを表示しています)"
126
+ end
127
+ else
128
+ io.puts "#{entry.full_name}: ページを取得できませんでした (#{html_url})"
129
+ end
130
+ io.puts
131
+ end
132
+
133
+ def not_found_message(words, class_only)
134
+ pattern = words.join(' ')
135
+ if class_only
136
+ "no such class: #{pattern}"
137
+ elsif words.size >= 2
138
+ "no such method in #{words.first}: #{words.last}"
139
+ elsif pattern.match?(/[\#,]\.|\.[\#,]|[\#\.\,]/)
140
+ "no such method: #{pattern}"
141
+ else
142
+ "no such class or method: #{pattern}"
143
+ end
144
+ end
145
+
146
+ # 索引の 1 エントリ。メソッド系(instance_method・class_method・constant・
147
+ # variable)は path("method/<クラス>/<種別 1 文字>/<名前>.html")から
148
+ # klass と typechar を持つ
149
+ class Entry
150
+ attr_reader :name, :full_name, :type, :path
151
+ attr_accessor :klass, :typechar
152
+
153
+ def initialize(name:, full_name:, type:, path:, klass: nil, typechar: nil)
154
+ @name = name
155
+ @full_name = full_name
156
+ @type = type
157
+ @path = path
158
+ @klass = klass
159
+ @typechar = typechar
160
+ end
161
+ end
162
+
163
+ # search_data.js を読んで refe と同じ規則で検索する
164
+ class Index
165
+ include NameUtils
166
+
167
+ CLASS_TYPES = %w[class module object].freeze
168
+ METHOD_TYPES = %w[instance_method class_method constant variable].freeze
169
+ METHOD_PATH_RE = %r{\Amethod/([^/]+)/([a-z])/([^/]+)\.html\z}
170
+
171
+ def self.parse(js)
172
+ json = js.sub(/\A\s*var\s+search_data\s*=\s*/, '').sub(/;\s*\z/, '')
173
+ data = JSON.parse(json)
174
+ entries = data['index'].map {|h|
175
+ entry = Entry.new(name: h['name'], full_name: h['full_name'], type: h['type'], path: h['path'])
176
+ if METHOD_TYPES.include?(entry.type) && (m = METHOD_PATH_RE.match(entry.path))
177
+ entry.klass = NameUtils.decodename_url(m[1] || raise)
178
+ entry.typechar = m[2]
179
+ end
180
+ entry
181
+ }
182
+ new(entries)
183
+ end
184
+
185
+ def initialize(entries)
186
+ @classes = entries.select {|e| CLASS_TYPES.include?(e.type) }
187
+ @methods = entries.select {|e| METHOD_TYPES.include?(e.type) && e.klass }
188
+ @libraries = entries.select {|e| e.type == 'library' }
189
+ @functions = entries.select {|e| e.type == 'function' }
190
+ end
191
+
192
+ # words は refe の引数(0〜3 語)。Searcher#search_pattern と同じ分岐
193
+ def search(words, class_only: false)
194
+ if class_only
195
+ raise InvalidKey, '--class option requires only 1 argument' unless words.size == 1
196
+ return narrow(@classes, words[0] || raise, &:full_name)
197
+ end
198
+ case words.size
199
+ when 0
200
+ @classes.dup
201
+ when 1
202
+ search_one(words[0] || raise)
203
+ when 2
204
+ search_method(words[0] || raise, nil, words[1] || raise)
205
+ when 3
206
+ c, t, m = words[0] || raise, words[1] || raise, words[2] || raise
207
+ raise InvalidKey, "'$' cannot be used as method type" if t == '$'
208
+ raise InvalidKey, "unknown method type: #{t.inspect}" unless typemark?(t)
209
+ search_method(c, t, m)
210
+ else
211
+ raise InvalidKey, "too many arguments (#{words.size} for 3)"
212
+ end
213
+ end
214
+
215
+ private
216
+
217
+ # Searcher#find_class_or_method と同じ優先順位。ライブラリと C の関数は
218
+ # refe のローカル検索には無い追加の fallback
219
+ def search_one(pat)
220
+ case pat
221
+ when /\A\$/ # 特殊変数
222
+ narrow(@methods.select {|e| e.typechar == 'v' }, pat, &:name)
223
+ when /[\#,]\.|\.[\#,]|[\#\.\,]/ # メソッド指定
224
+ c, t, m = parse_method_spec_pattern(pat)
225
+ search_method(c, t, m)
226
+ when /::/ # クラス名か定数名
227
+ found = narrow(@classes, pat, &:full_name)
228
+ return found unless found.empty?
229
+ names = pat.split('::')
230
+ name = names.pop || raise
231
+ search_method(names.join('::'), '::', name)
232
+ when /\A[A-Z]/ # クラス名が優先
233
+ found = narrow(@classes, pat, &:full_name)
234
+ found = search_method(nil, nil, pat) if found.empty?
235
+ found
236
+ else
237
+ found = search_method(nil, nil, pat)
238
+ found = narrow(@classes, pat, &:full_name) if found.empty?
239
+ found = narrow(@libraries, pat, &:full_name) if found.empty?
240
+ found = narrow(@functions, pat, &:full_name) if found.empty?
241
+ found
242
+ end
243
+ end
244
+
245
+ def search_method(c, t, m)
246
+ entries = @methods
247
+ if c
248
+ klasses = narrow(@classes, c, &:full_name).map(&:full_name)
249
+ return [] if klasses.empty?
250
+ entries = entries.select {|e| (k = e.klass) && klasses.include?(k) }
251
+ end
252
+ if t
253
+ typechar = typemark2char(_ = t)
254
+ entries = entries.select {|e| e.typechar == typechar }
255
+ end
256
+ narrow(entries, m, &:name)
257
+ end
258
+
259
+ # Completion#expand と同じ絞り込み: 大小無視の前方一致 → 大小無視の
260
+ # 完全一致 → 完全一致。絞れなければ手前の結果を返す
261
+ def narrow(xs, pattern)
262
+ re1 = /\A#{Regexp.quote(pattern)}/i
263
+ result1 = xs.select {|x| yield(x).match?(re1) }
264
+ return result1 if result1.size <= 1
265
+ re2 = /\A#{Regexp.quote(pattern)}\z/i
266
+ result2 = result1.select {|x| yield(x).match?(re2) }
267
+ return result1 if result2.empty?
268
+ return result2 if result2.size == 1
269
+ result3 = result2.select {|x| yield(x) == pattern }
270
+ result3.empty? ? result2 : result3
271
+ end
272
+ end
273
+
274
+ # URL ごとのファイルキャッシュ(dir/<ホスト>/<パス>)。mtime は最後に
275
+ # 確認した時刻。max_age 以内ならそのまま使い、古ければ If-Modified-Since
276
+ # で確認する(304 なら touch)。取得できなければ古いものを stale として返す
277
+ class Cache
278
+ MAX_AGE = 24 * 60 * 60
279
+
280
+ # 取得結果。stale は接続できずに古いキャッシュを返したとき true
281
+ class Hit
282
+ attr_reader :body, :path, :stale
283
+
284
+ def initialize(body:, path:, stale:)
285
+ @body = body
286
+ @path = path
287
+ @stale = stale
288
+ end
289
+ end
290
+
291
+ def initialize(dir:, fetcher:, max_age: MAX_AGE, clock: -> { Time.now })
292
+ @dir = dir
293
+ @fetcher = fetcher
294
+ @max_age = max_age
295
+ @clock = clock
296
+ end
297
+
298
+ # 本文を Hit で返す。404 なら nil。キャッシュが無くて取得にも失敗したら
299
+ # 例外(Remote::NETWORK_ERRORS のどれか)
300
+ def fetch(uri)
301
+ path = path_for(uri)
302
+ now = @clock.call
303
+ cached = File.file?(path)
304
+ if cached
305
+ mtime = File.mtime(path)
306
+ return Hit.new(body: read(path), path: path, stale: false) if mtime > now - @max_age
307
+ headers = { 'If-Modified-Since' => mtime.httpdate }
308
+ else
309
+ headers = {} #: Hash[String, String]
310
+ end
311
+ begin
312
+ status, body = @fetcher.call(uri, headers)
313
+ rescue *NETWORK_ERRORS
314
+ raise unless cached
315
+ return Hit.new(body: read(path), path: path, stale: true)
316
+ end
317
+ case status
318
+ when 200
319
+ text = to_utf8(body)
320
+ write(path, text, now)
321
+ Hit.new(body: text, path: path, stale: false)
322
+ when 304
323
+ raise HTTPError, "304 without a cached copy: #{uri}" unless cached
324
+ File.utime(now, now, path)
325
+ Hit.new(body: read(path), path: path, stale: false)
326
+ when 404
327
+ File.delete(path) if cached
328
+ nil
329
+ else
330
+ return Hit.new(body: read(path), path: path, stale: true) if cached
331
+ raise HTTPError, "HTTP #{status}: #{uri}"
332
+ end
333
+ end
334
+
335
+ private
336
+
337
+ def path_for(uri)
338
+ File.join(@dir, uri.host || raise, uri.path || raise)
339
+ end
340
+
341
+ def read(path)
342
+ File.read(path, encoding: Encoding::UTF_8)
343
+ end
344
+
345
+ def write(path, text, now)
346
+ FileUtils.mkdir_p(File.dirname(path))
347
+ tmp = "#{path}.tmp"
348
+ File.binwrite(tmp, text)
349
+ File.rename(tmp, path)
350
+ File.utime(now, now, path)
351
+ end
352
+
353
+ # 配信は text/markdown; charset=utf-8。Net::HTTP の本文は BINARY で返る
354
+ def to_utf8(body)
355
+ str = body.dup.force_encoding(Encoding::UTF_8)
356
+ str.valid_encoding? ? str : str.scrub
357
+ end
358
+ end
359
+
360
+ # Net::HTTP による既定の取得。同じホストへの接続は使い回し、close で閉じる。
361
+ # gzip は Net::HTTP が自動で展開する(search_data.js は約 3MB・gzip 420KB)
362
+ class HttpFetcher
363
+ HEADERS = {
364
+ 'User-Agent' => "bitclust/#{BitClust::VERSION}",
365
+ 'Accept' => 'text/markdown, text/javascript, */*',
366
+ }.freeze
367
+ MAX_REDIRECTS = 2
368
+
369
+ def initialize(open_timeout: 5, read_timeout: 30)
370
+ @open_timeout = open_timeout
371
+ @read_timeout = read_timeout
372
+ @sessions = {}
373
+ end
374
+
375
+ def call(uri, headers = {})
376
+ redirects = 0
377
+ loop do
378
+ res = session(uri).get(uri.path || raise, HEADERS.merge(headers))
379
+ location = res['location']
380
+ if res.is_a?(Net::HTTPRedirection) && location && redirects < MAX_REDIRECTS
381
+ redirects += 1
382
+ uri = URI.join(uri, location)
383
+ else
384
+ return [res.code.to_i, res.body.to_s]
385
+ end
386
+ end
387
+ end
388
+
389
+ def close
390
+ @sessions.each_value {|http| http.finish if http.started? }
391
+ @sessions.clear
392
+ end
393
+
394
+ private
395
+
396
+ def session(uri)
397
+ @sessions[[uri.host, uri.port]] ||=
398
+ Net::HTTP.start(uri.host || raise, uri.port,
399
+ use_ssl: uri.scheme == 'https',
400
+ open_timeout: @open_timeout,
401
+ read_timeout: @read_timeout)
402
+ end
403
+ end
404
+ end
405
+ end
@@ -14,6 +14,7 @@ $LOAD_PATH.unshift(libdir.to_s)
14
14
 
15
15
  require 'bitclust'
16
16
  require 'bitclust/subcommand'
17
+ require 'bitclust/user_dirs'
17
18
 
18
19
  subcommands_dir = libdir + "bitclust/subcommands"
19
20
  Dir.glob(File.join(subcommands_dir.to_s, "*.rb")) do |entry|
@@ -62,6 +63,7 @@ Subcommands(for developers):
62
63
  classes Display defined classes for all ruby.
63
64
  methods Display defined methods for all ruby.
64
65
  methodsince Fill per-name since/until from a version ladder of DBs.
66
+ rbssig Fill RBS type signatures into a 4.0+ DB from .rbs files.
65
67
  checklink Report broken [[c:]]/[[m:]]/[[lib:]]/[[d:]]/[[f:]] links.
66
68
 
67
69
  Subcommands(for packagers):
@@ -110,6 +112,7 @@ Global Options:
110
112
  'classes' => BitClust::Subcommands::ClassesCommand.new,
111
113
  'methods' => BitClust::Subcommands::MethodsCommand.new,
112
114
  'methodsince' => BitClust::Subcommands::MethodsinceCommand.new,
115
+ 'rbssig' => BitClust::Subcommands::RbssigCommand.new,
113
116
  'checklink' => BitClust::Subcommands::ChecklinkCommand.new,
114
117
  }
115
118
  end
@@ -141,12 +144,12 @@ Global Options:
141
144
  @version ||= config[:default_version]
142
145
  @prefix ||= "#{config[:database_prefix]}-#{@version}"
143
146
  end
144
- # DB 必須のサブコマンドで --database も ~/.bitclust/config も無ければ
145
- # 案内付きで中断する。needs_database? が false のサブコマンドと、
147
+ # DB 必須のサブコマンドで --database も設定ファイル(UserDirs)も
148
+ # 無ければ案内付きで中断する。needs_database? が false のサブコマンドと、
146
149
  # 自前で DB を探す search (Searcher) は対象外
147
150
  needs_database = cmd.respond_to?(:needs_database?) && cmd.needs_database?
148
151
  if needs_database && !@prefix
149
- error "no database given. Use --database (-d) option or ~/.bitclust/config"
152
+ error "no database given. Use --database (-d) option or run `bitclust setup` (config: #{UserDirs.config_candidates.join(' or ')})"
150
153
  end
151
154
  # @type var options: Subcommand::options
152
155
  options = {
@@ -160,13 +163,7 @@ Global Options:
160
163
  end
161
164
 
162
165
  def load_config
163
- home_directory = Pathname(ENV.fetch('HOME'))
164
- config_path = home_directory + ".bitclust/config"
165
- if config_path.exist?
166
- YAML.load_file(config_path)
167
- else
168
- nil
169
- end
166
+ UserDirs.load_config
170
167
  end
171
168
 
172
169
  def error(message)
@@ -403,6 +403,10 @@ module BitClust
403
403
  @urlmapper.search_url
404
404
  end
405
405
 
406
+ def document_url(name)
407
+ @urlmapper.document_url(name)
408
+ end
409
+
406
410
  def library_index_url
407
411
  @urlmapper.library_index_url
408
412
  end
@@ -650,7 +654,8 @@ module BitClust
650
654
  attr_reader :search_full_url
651
655
 
652
656
  def body
653
- run_template('opensearchdescription')
657
+ # XML なので HTML の layout は通さない
658
+ run_template('opensearchdescription', false)
654
659
  end
655
660
 
656
661
  def content_type
@@ -114,6 +114,9 @@ module BitClust
114
114
  key = e.values_at(:name, :full_name, :type, :path)
115
115
  entry = (merged[key] ||= e.merge(versions: []))
116
116
  entry[:versions] << version
117
+ # 説明文は「最新版か、収録されている最終版」のものを採用する。
118
+ # 昇順走査なので後勝ちで上書きすればよい
119
+ entry[:snippet] = e[:snippet] if e[:snippet]
117
120
  end
118
121
  end
119
122
  merged.values
@@ -126,14 +129,29 @@ module BitClust
126
129
 
127
130
  private
128
131
 
132
+ # 検索結果に添える説明文。meta description と同じ entry.description
133
+ # (最初の説明段落のプレーンテキスト)を使い、空なら nil(キー自体を
134
+ # 持たせない)。プレーンテキストのまま格納し、HTML エスケープは
135
+ # 表示側(search_init.js / search_page.js)で行う
136
+ def entry_snippet(entry)
137
+ desc = entry.description
138
+ desc unless desc.nil? || desc.empty?
139
+ end
140
+
141
+ def with_snippet(hash, entry)
142
+ snippet = entry_snippet(entry)
143
+ hash[:snippet] = snippet if snippet
144
+ hash
145
+ end
146
+
129
147
  def class_entries(db)
130
148
  db.classes.sort.reject(&:dummy?).map do |c|
131
- {
149
+ with_snippet({
132
150
  name: c.name,
133
151
  full_name: c.name,
134
152
  type: c.type.to_s,
135
153
  path: "class/#{encode(c.name)}#{@suffix}",
136
- }
154
+ }, c)
137
155
  end
138
156
  end
139
157
 
@@ -162,11 +180,11 @@ module BitClust
162
180
  # sigil is the only thing distinguishing it, so keep it in +name+
163
181
  # too (not just +full_name+) or a "$;"-style query can't match it.
164
182
  name = full_name = tmark + mname
165
- result << { name: name, full_name: full_name, type: type, path: path }
183
+ result << with_snippet({ name: name, full_name: full_name, type: type, path: path }, entry)
166
184
  else
167
185
  name = mname
168
186
  full_name = cname + tmark + mname
169
- item = { name: name, full_name: full_name, type: type, path: path } #: entry
187
+ item = with_snippet({ name: name, full_name: full_name, type: type, path: path }, entry) #: entry
170
188
  if tmark.include?('.')
171
189
  # bitclust#279: singleton methods (".") and module functions
172
190
  # (".#"/"?.") keep a literal "." in full_name for display, but
@@ -185,12 +203,12 @@ module BitClust
185
203
 
186
204
  def library_entries(db)
187
205
  db.libraries.sort.map do |lib|
188
- {
206
+ with_snippet({
189
207
  name: lib.name,
190
208
  full_name: lib.name,
191
209
  type: 'library',
192
210
  path: "library/#{encode(lib.name)}#{@suffix}",
193
- }
211
+ }, lib)
194
212
  end
195
213
  end
196
214
 
@@ -201,12 +219,12 @@ module BitClust
201
219
  # Prose pages are identified by a Japanese title, so index the title
202
220
  # (for human queries) together with the slug (for identifier queries).
203
221
  label = (title && !title.empty? && title != slug) ? "#{title} (#{slug})" : slug
204
- {
222
+ with_snippet({
205
223
  name: label,
206
224
  full_name: label,
207
225
  type: 'document',
208
226
  path: "doc/#{encode(slug)}#{@suffix}",
209
- }
227
+ }, doc)
210
228
  end
211
229
  end
212
230
 
@@ -272,12 +290,12 @@ module BitClust
272
290
 
273
291
  def function_entries(fdb)
274
292
  fdb.functions.sort.map do |func|
275
- {
293
+ with_snippet({
276
294
  name: func.name,
277
295
  full_name: func.name,
278
296
  type: 'function',
279
297
  path: "function/#{func.name}#{@suffix}",
280
- }
298
+ }, func)
281
299
  end
282
300
  end
283
301