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.
- checksums.yaml +4 -4
- data/data/bitclust/template/class-index +0 -1
- data/data/bitclust/template.epub/class +5 -5
- data/data/bitclust/template.epub/class-index +0 -1
- data/data/bitclust/template.epub/contents +7 -4
- data/data/bitclust/template.epub/layout +4 -4
- data/data/bitclust/template.offline/class-index +0 -1
- data/lib/bitclust/generators/epub.rb +69 -5
- data/lib/bitclust/irb.rb +14 -8
- data/lib/bitclust/markdown_bridge.rb +11 -6
- data/lib/bitclust/markdown_tree.rb +118 -11
- data/lib/bitclust/methoddatabase.rb +17 -4
- data/lib/bitclust/nameutils.rb +13 -0
- data/lib/bitclust/preprocessor.rb +0 -36
- data/lib/bitclust/remote.rb +405 -0
- data/lib/bitclust/runner.rb +5 -10
- data/lib/bitclust/screen.rb +2 -1
- data/lib/bitclust/searcher.rb +43 -24
- data/lib/bitclust/subcommands/chm_command.rb +1 -1
- data/lib/bitclust/subcommands/epub_command.rb +2 -2
- data/lib/bitclust/subcommands/extract_command.rb +7 -0
- data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
- data/lib/bitclust/subcommands/methods_command.rb +5 -28
- data/lib/bitclust/subcommands/query_command.rb +1 -0
- data/lib/bitclust/subcommands/server_command.rb +21 -10
- data/lib/bitclust/subcommands/setup_command.rb +28 -10
- data/lib/bitclust/syntax_highlighter.rb +65 -4
- data/lib/bitclust/user_dirs.rb +62 -0
- data/lib/bitclust/version.rb +1 -1
- data/test/test_chm_command.rb +22 -0
- data/test/test_epub_command.rb +60 -0
- data/test/test_epub_generator.rb +330 -0
- data/test/test_extract_command.rb +45 -0
- data/test/test_irb_plugin.rb +100 -6
- data/test/test_markdown_bridge.rb +35 -0
- data/test/test_markdown_tree.rb +144 -0
- data/test/test_mdparser.rb +39 -0
- data/test/test_methods_command.rb +32 -0
- data/test/test_opensearchdescription_screen.rb +39 -0
- data/test/test_query_command.rb +30 -0
- data/test/test_remote.rb +374 -0
- data/test/test_runner.rb +61 -1
- data/test/test_searcher.rb +63 -0
- data/test/test_server_command.rb +41 -0
- data/test/test_setup_command.rb +143 -0
- data/test/test_syntax_highlighter.rb +124 -0
- data/test/test_user_dirs.rb +62 -0
- metadata +37 -2
- 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
|
data/lib/bitclust/runner.rb
CHANGED
|
@@ -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 も
|
|
147
|
-
#
|
|
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
|
|
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
|
-
|
|
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)
|
data/lib/bitclust/screen.rb
CHANGED
data/lib/bitclust/searcher.rb
CHANGED
|
@@ -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
|
-
# 既定の場所(
|
|
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
|
-
|
|
255
|
-
if
|
|
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 =
|
|
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 => @
|
|
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)
|