bitclust-core 1.7.0 → 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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/data/bitclust/template/class-index +0 -1
  3. data/data/bitclust/template.epub/class +5 -5
  4. data/data/bitclust/template.epub/class-index +0 -1
  5. data/data/bitclust/template.epub/contents +7 -4
  6. data/data/bitclust/template.epub/layout +4 -4
  7. data/data/bitclust/template.offline/class-index +0 -1
  8. data/lib/bitclust/generators/epub.rb +69 -5
  9. data/lib/bitclust/irb.rb +14 -8
  10. data/lib/bitclust/markdown_bridge.rb +11 -6
  11. data/lib/bitclust/markdown_tree.rb +118 -11
  12. data/lib/bitclust/methoddatabase.rb +17 -4
  13. data/lib/bitclust/nameutils.rb +13 -0
  14. data/lib/bitclust/preprocessor.rb +0 -36
  15. data/lib/bitclust/remote.rb +405 -0
  16. data/lib/bitclust/runner.rb +5 -10
  17. data/lib/bitclust/screen.rb +2 -1
  18. data/lib/bitclust/searcher.rb +43 -24
  19. data/lib/bitclust/subcommands/chm_command.rb +1 -1
  20. data/lib/bitclust/subcommands/epub_command.rb +2 -2
  21. data/lib/bitclust/subcommands/extract_command.rb +7 -0
  22. data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
  23. data/lib/bitclust/subcommands/methods_command.rb +5 -28
  24. data/lib/bitclust/subcommands/query_command.rb +1 -0
  25. data/lib/bitclust/subcommands/server_command.rb +21 -10
  26. data/lib/bitclust/subcommands/setup_command.rb +28 -10
  27. data/lib/bitclust/syntax_highlighter.rb +65 -4
  28. data/lib/bitclust/user_dirs.rb +62 -0
  29. data/lib/bitclust/version.rb +1 -1
  30. data/test/test_chm_command.rb +22 -0
  31. data/test/test_epub_command.rb +60 -0
  32. data/test/test_epub_generator.rb +330 -0
  33. data/test/test_extract_command.rb +45 -0
  34. data/test/test_irb_plugin.rb +100 -6
  35. data/test/test_markdown_bridge.rb +35 -0
  36. data/test/test_markdown_tree.rb +144 -0
  37. data/test/test_mdparser.rb +39 -0
  38. data/test/test_methods_command.rb +32 -0
  39. data/test/test_opensearchdescription_screen.rb +39 -0
  40. data/test/test_query_command.rb +30 -0
  41. data/test/test_remote.rb +374 -0
  42. data/test/test_runner.rb +61 -1
  43. data/test/test_searcher.rb +63 -0
  44. data/test/test_server_command.rb +41 -0
  45. data/test/test_setup_command.rb +143 -0
  46. data/test/test_syntax_highlighter.rb +124 -0
  47. data/test/test_user_dirs.rb +62 -0
  48. metadata +37 -2
  49. data/lib/bitclust/ridatabase.rb +0 -359
@@ -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|
@@ -143,12 +144,12 @@ Global Options:
143
144
  @version ||= config[:default_version]
144
145
  @prefix ||= "#{config[:database_prefix]}-#{@version}"
145
146
  end
146
- # DB 必須のサブコマンドで --database も ~/.bitclust/config も無ければ
147
- # 案内付きで中断する。needs_database? が false のサブコマンドと、
147
+ # DB 必須のサブコマンドで --database も設定ファイル(UserDirs)も
148
+ # 無ければ案内付きで中断する。needs_database? が false のサブコマンドと、
148
149
  # 自前で DB を探す search (Searcher) は対象外
149
150
  needs_database = cmd.respond_to?(:needs_database?) && cmd.needs_database?
150
151
  if needs_database && !@prefix
151
- 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 ')})"
152
153
  end
153
154
  # @type var options: Subcommand::options
154
155
  options = {
@@ -162,13 +163,7 @@ Global Options:
162
163
  end
163
164
 
164
165
  def load_config
165
- home_directory = Pathname(ENV.fetch('HOME'))
166
- config_path = home_directory + ".bitclust/config"
167
- if config_path.exist?
168
- YAML.load_file(config_path)
169
- else
170
- nil
171
- end
166
+ UserDirs.load_config
172
167
  end
173
168
 
174
169
  def error(message)
@@ -654,7 +654,8 @@ module BitClust
654
654
  attr_reader :search_full_url
655
655
 
656
656
  def body
657
- run_template('opensearchdescription')
657
+ # XML なので HTML の layout は通さない
658
+ run_template('opensearchdescription', false)
658
659
  end
659
660
 
660
661
  def content_type
@@ -9,10 +9,12 @@
9
9
  #
10
10
 
11
11
  require 'bitclust/methoddatabase'
12
+ require 'bitclust/remote'
12
13
  require 'bitclust/functiondatabase'
13
14
  require 'bitclust/nameutils'
14
15
  require 'bitclust/methodid'
15
16
  require 'bitclust/exception'
17
+ require 'bitclust/user_dirs'
16
18
  require 'uri'
17
19
  require 'rbconfig'
18
20
  require 'optparse'
@@ -36,6 +38,7 @@ module BitClust
36
38
  @target_type = nil
37
39
  @listen_url = nil
38
40
  @foreground = false
41
+ @remote = Remote.default
39
42
  @parser = OptionParser.new {|parser|
40
43
  parser.banner = "Usage: #{@name} <pattern>"
41
44
  unless cmd == 'bitclust'
@@ -91,6 +94,10 @@ module BitClust
91
94
 
92
95
  attr_reader :parser
93
96
 
97
+ # ローカル DB が無いときに使うリモート検索(BitClust::Remote)。nil なら
98
+ # DB が無いことをエラーにする
99
+ attr_accessor :remote
100
+
94
101
  def parse(argv)
95
102
  @parser.parse! argv
96
103
  if @listen_url # server mode
@@ -123,12 +130,20 @@ module BitClust
123
130
 
124
131
  # irb プラグインなど組み込み利用向けの入口。argv のパターンを db から
125
132
  # 検索し、結果を view(TerminalView)へ出力する。db が nil なら
126
- # 既定の場所(~/.bitclust/config など)から探す
133
+ # 既定の場所(環境変数 BITCLUST_DATADIR など・設定ファイル(UserDirs))
134
+ # から探す
127
135
  def run_query(db, argv, view)
128
136
  @view = view
129
137
  search_pattern db, argv
130
138
  end
131
139
 
140
+ # 既定の場所(環境変数 BITCLUST_DATADIR など・設定ファイル(UserDirs))に
141
+ # ローカル DB があるか。irb プラグインが HTTP フォールバックへ切り替える
142
+ # 判定に使う。環境変数が不正な DB を指していれば InvalidDatabase
143
+ def local_database?
144
+ !find_dblocation().nil?
145
+ end
146
+
132
147
  private
133
148
 
134
149
  def server_mode_check(argv)
@@ -251,15 +266,23 @@ module BitClust
251
266
  [ "#{datadir}/refe2", "#{datadir}/bitclust" ].each do |path|
252
267
  return path if MethodDatabase.datadir?(path)
253
268
  end
254
- config_path = Pathname(ENV.fetch('HOME')) + ".bitclust" + "config"
255
- if config_path.exist?
256
- config = YAML.load_file(config_path)
269
+ config = UserDirs.load_config
270
+ if config
257
271
  return "#{config[:database_prefix]}-#{config[:default_version]}"
258
272
  end
259
273
  nil
260
274
  end
261
275
 
262
276
  def search_pattern(db, argv)
277
+ if db.nil? && !find_dblocation
278
+ remote = @remote
279
+ unless remote
280
+ raise InvalidDatabase, "no database found: run `bitclust setup` to create one (remote search is disabled by BITCLUST_REMOTE_URL)"
281
+ end
282
+ remote.lookup(argv, @view.io, describe_all: @describe_all, line: @linep,
283
+ class_only: @target_type == :class)
284
+ return
285
+ end
263
286
  db ||= new_database()
264
287
  @view.database ||= db if @view
265
288
  # FIXME なぜか else 節にきかないのでここに書いておく
@@ -360,16 +383,6 @@ module BitClust
360
383
  find_method db, cnames.join('::'), '::', name
361
384
  end
362
385
 
363
- # 区切りは反転文字列上で探す。"?." は 4.0 以降の module function の
364
- # 表示形式(反転すると ".?")で、内部表記の ".#" に正規化する
365
- def parse_method_spec_pattern(pat)
366
- _m, _t, _c = pat.reverse.split(/([\#,]\.|\.[\#,]|\.\?|[\#\.\,])/, 2)
367
- c = (_c || raise).reverse
368
- t = (_t || raise).tr(',', '#').sub(/\#\.|\.\?/, '.#')
369
- m = (_m || raise).reverse
370
- return c, t, m
371
- end
372
-
373
386
  end
374
387
 
375
388
 
@@ -394,6 +407,22 @@ module BitClust
394
407
 
395
408
  attr_accessor :database
396
409
 
410
+ # 出力先(io: 省略時は $stdout)
411
+ def io
412
+ @io || $stdout
413
+ end
414
+
415
+ # 名前の一覧を表示する(-l なら 1 行 1 件、それ以外は端末幅に詰める)
416
+ def print_names(names)
417
+ if @line
418
+ names.sort.each do |n|
419
+ puts n
420
+ end
421
+ else
422
+ print_packed_names names.sort
423
+ end
424
+ end
425
+
397
426
  def show_class(cs)
398
427
  if cs.size == 1
399
428
  if @line
@@ -450,16 +479,6 @@ module BitClust
450
479
 
451
480
  private
452
481
 
453
- def print_names(names)
454
- if @line
455
- names.sort.each do |n|
456
- puts n
457
- end
458
- else
459
- print_packed_names names.sort
460
- end
461
- end
462
-
463
482
  def print_packed_names(names)
464
483
  max = terminal_column()
465
484
  buf = +''
@@ -102,7 +102,7 @@ EOS
102
102
  end
103
103
 
104
104
  def to_html
105
- str = "<LI> <OBJECT type=\"text/sitemap\">\n"
105
+ str = +"<LI> <OBJECT type=\"text/sitemap\">\n"
106
106
  str << " <param name=\"Name\" value=\"<%=h @name%>\">\n"
107
107
  if @local
108
108
  str << " <param name=\"Local\" value=\"<%=@local%>\">\n"
@@ -12,7 +12,7 @@ module BitClust
12
12
  def initialize
13
13
  super
14
14
  @verbose = true
15
- @catalogdir = nil
15
+ @catalogdir = srcdir_root + "data/bitclust/catalog"
16
16
  @templatedir = srcdir_root + "data/bitclust/template.epub"
17
17
  @themedir = srcdir_root + "theme/default"
18
18
  @filename = "rurema-#{Date.today}.epub"
@@ -54,7 +54,7 @@ module BitClust
54
54
  generator = BitClust::Generators::EPUB.new(:prefix => options[:prefix],
55
55
  :capi => options[:capi],
56
56
  :outputdir => @outputdir,
57
- :catalog => @catalog,
57
+ :catalog => @catalogdir,
58
58
  :templatedir => @templatedir,
59
59
  :themedir => @themedir,
60
60
  :fs_casesensitive => @fs_casesensitive,
@@ -25,6 +25,7 @@ module BitClust
25
25
  def exec(argv, options)
26
26
  success = true
27
27
  argv.each do |path|
28
+ reject_markdown_source(path)
28
29
  begin
29
30
  lib = RRDParser.parse_stdlib_file(path)
30
31
  if @check_only
@@ -41,6 +42,12 @@ module BitClust
41
42
  exit success
42
43
  end
43
44
 
45
+ # 旧 RD ソース(refm)専用なので、Markdown は未対応として案内する
46
+ def reject_markdown_source(path)
47
+ return unless path.end_with?('.md')
48
+ error "#{path}: extract は旧 RD ソース(refm)専用で、Markdown(manual/ の .md)には未対応です。manual/ の内容は doctree の rake generate:X.Y と bitclust server で確認してください"
49
+ end
50
+
44
51
  def show_library(lib)
45
52
  puts "= Library #{lib.name}"
46
53
  lib.classes.each do |c|
@@ -47,6 +47,7 @@ module BitClust
47
47
  end
48
48
  @capi = options[:capi]
49
49
  target_file = argv[0]
50
+ reject_markdown_source(target_file)
50
51
  if /refm\/doc\// =~ target_file
51
52
  @rd_file = true if @rd_file.nil?
52
53
  end
@@ -88,6 +89,12 @@ module BitClust
88
89
 
89
90
  private
90
91
 
92
+ # 旧 RD ソース(refm)専用なので、Markdown は未対応として案内する
93
+ def reject_markdown_source(path)
94
+ return unless path && path.end_with?('.md')
95
+ error "#{path}: htmlfile は旧 RD ソース(refm)専用で、Markdown(manual/ の .md)には未対応です。manual/ の内容は doctree の rake generate:X.Y と bitclust server(または rake statichtml:X.Y)で確認してください"
96
+ end
97
+
91
98
  def lookup(lib, key)
92
99
  case
93
100
  when @capi && NameUtils.functionname?(key)