bitclust-core 1.2.6 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/README.md +65 -0
  4. data/Rakefile +32 -0
  5. data/data/bitclust/catalog/ja_JP.UTF-8 +19 -1
  6. data/data/bitclust/searchpage/index.html +90 -0
  7. data/data/bitclust/template/class +69 -8
  8. data/data/bitclust/template/function +2 -2
  9. data/data/bitclust/template/layout +11 -3
  10. data/data/bitclust/template/method +2 -2
  11. data/data/bitclust/template/search +1 -1
  12. data/data/bitclust/template.epub/class +22 -1
  13. data/data/bitclust/template.epub/function +2 -2
  14. data/data/bitclust/template.epub/layout +1 -3
  15. data/data/bitclust/template.epub/method +2 -2
  16. data/data/bitclust/template.lillia/class +26 -5
  17. data/data/bitclust/template.lillia/layout +1 -3
  18. data/data/bitclust/template.lillia/method +2 -2
  19. data/data/bitclust/template.offline/class +49 -5
  20. data/data/bitclust/template.offline/function +2 -2
  21. data/data/bitclust/template.offline/layout +27 -3
  22. data/data/bitclust/template.offline/method +2 -2
  23. data/lib/bitclust/app.rb +17 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +68 -17
  26. data/lib/bitclust/compat.rb +3 -2
  27. data/lib/bitclust/completion.rb +59 -27
  28. data/lib/bitclust/crossrubyutils.rb +9 -5
  29. data/lib/bitclust/database.rb +29 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +2 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +48 -7
  34. data/lib/bitclust/exception.rb +1 -0
  35. data/lib/bitclust/functiondatabase.rb +14 -1
  36. data/lib/bitclust/functionentry.rb +6 -1
  37. data/lib/bitclust/functionreferenceparser.rb +5 -4
  38. data/lib/bitclust/generators/epub.rb +11 -3
  39. data/lib/bitclust/htmlutils.rb +2 -1
  40. data/lib/bitclust/include_graph.rb +375 -0
  41. data/lib/bitclust/include_pruner.rb +144 -0
  42. data/lib/bitclust/interface.rb +2 -1
  43. data/lib/bitclust/libraryentry.rb +9 -7
  44. data/lib/bitclust/lineinput.rb +8 -5
  45. data/lib/bitclust/link_checker.rb +179 -0
  46. data/lib/bitclust/markdown_bridge.rb +186 -0
  47. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  48. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  49. data/lib/bitclust/markdown_tree.rb +217 -0
  50. data/lib/bitclust/mdcompiler.rb +883 -0
  51. data/lib/bitclust/mdparser.rb +347 -0
  52. data/lib/bitclust/messagecatalog.rb +4 -3
  53. data/lib/bitclust/method_since_calculator.rb +117 -0
  54. data/lib/bitclust/methoddatabase.rb +125 -14
  55. data/lib/bitclust/methodentry.rb +130 -7
  56. data/lib/bitclust/methodid.rb +13 -11
  57. data/lib/bitclust/methodsignature.rb +11 -4
  58. data/lib/bitclust/nameutils.rb +42 -22
  59. data/lib/bitclust/parseutils.rb +1 -0
  60. data/lib/bitclust/preprocessor.rb +70 -32
  61. data/lib/bitclust/progress_bar.rb +1 -0
  62. data/lib/bitclust/rdcompiler.rb +197 -96
  63. data/lib/bitclust/refsdatabase.rb +24 -8
  64. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  65. data/lib/bitclust/requesthandler.rb +22 -16
  66. data/lib/bitclust/ridatabase.rb +11 -4
  67. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  68. data/lib/bitclust/rrdparser.rb +197 -30
  69. data/lib/bitclust/runner.rb +18 -2
  70. data/lib/bitclust/screen.rb +81 -11
  71. data/lib/bitclust/search_index_generator.rb +288 -0
  72. data/lib/bitclust/searcher.rb +72 -22
  73. data/lib/bitclust/server.rb +6 -1
  74. data/lib/bitclust/silent_progress_bar.rb +1 -0
  75. data/lib/bitclust/simplesearcher.rb +25 -12
  76. data/lib/bitclust/subcommand.rb +8 -1
  77. data/lib/bitclust/subcommands/ancestors_command.rb +10 -2
  78. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  79. data/lib/bitclust/subcommands/chm_command.rb +16 -7
  80. data/lib/bitclust/subcommands/classes_command.rb +6 -0
  81. data/lib/bitclust/subcommands/epub_command.rb +1 -0
  82. data/lib/bitclust/subcommands/extract_command.rb +6 -0
  83. data/lib/bitclust/subcommands/htmlfile_command.rb +22 -7
  84. data/lib/bitclust/subcommands/init_command.rb +5 -1
  85. data/lib/bitclust/subcommands/list_command.rb +13 -4
  86. data/lib/bitclust/subcommands/lookup_command.rb +29 -9
  87. data/lib/bitclust/subcommands/methods_command.rb +11 -2
  88. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  89. data/lib/bitclust/subcommands/preproc_command.rb +6 -0
  90. data/lib/bitclust/subcommands/property_command.rb +3 -1
  91. data/lib/bitclust/subcommands/query_command.rb +1 -0
  92. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  93. data/lib/bitclust/subcommands/server_command.rb +16 -10
  94. data/lib/bitclust/subcommands/setup_command.rb +25 -10
  95. data/lib/bitclust/subcommands/statichtml_command.rb +132 -6
  96. data/lib/bitclust/subcommands/update_command.rb +111 -2
  97. data/lib/bitclust/syntax_highlighter.rb +52 -5
  98. data/lib/bitclust/textutils.rb +4 -3
  99. data/lib/bitclust/version.rb +2 -1
  100. data/lib/bitclust/version_badges.rb +90 -0
  101. data/lib/bitclust/whole_file_gate.rb +137 -0
  102. data/lib/bitclust.rb +1 -0
  103. data/test/test_app.rb +269 -0
  104. data/test/test_bitclust.rb +2 -0
  105. data/test/test_capi_converter.rb +91 -0
  106. data/test/test_class_screen.rb +413 -0
  107. data/test/test_copy_doc_md.rb +53 -0
  108. data/test/test_doc_converter.rb +78 -0
  109. data/test/test_entity_splitter.rb +231 -0
  110. data/test/test_entry.rb +207 -2
  111. data/test/test_eol_warning.rb +43 -0
  112. data/test/test_functionreferenceparser.rb +11 -0
  113. data/test/test_include_graph.rb +640 -0
  114. data/test/test_include_pruner.rb +133 -0
  115. data/test/test_link_checker.rb +114 -0
  116. data/test/test_lookup_command.rb +135 -0
  117. data/test/test_markdown_bridge.rb +217 -0
  118. data/test/test_markdown_orchestrator.rb +478 -0
  119. data/test/test_markdown_to_rrd.rb +757 -0
  120. data/test/test_markdown_tree.rb +369 -0
  121. data/test/test_mdcompiler.rb +1276 -0
  122. data/test/test_mdparser.rb +538 -0
  123. data/test/test_method_screen.rb +166 -0
  124. data/test/test_method_since_calculator.rb +290 -0
  125. data/test/test_methoddatabase.rb +16 -0
  126. data/test/test_methodentry.rb +328 -0
  127. data/test/test_methodsignature.rb +3 -1
  128. data/test/test_methodsince_command.rb +168 -0
  129. data/test/test_nameutils.rb +22 -0
  130. data/test/test_preprocessor.rb +215 -0
  131. data/test/test_rdcompiler.rb +429 -27
  132. data/test/test_refsdatabase.rb +32 -0
  133. data/test/test_rrd_to_markdown.rb +968 -0
  134. data/test/test_rrdparser.rb +295 -2
  135. data/test/test_run_ruby_wasm.rb +54 -0
  136. data/test/test_run_worker_prelude.rb +83 -0
  137. data/test/test_runner.rb +51 -0
  138. data/test/test_search_index_generator.rb +513 -0
  139. data/test/test_search_screen.rb +48 -0
  140. data/test/test_searcher.rb +153 -0
  141. data/test/test_searchpage_command.rb +167 -0
  142. data/test/test_simplesearcher.rb +28 -0
  143. data/test/test_statichtml_command.rb +204 -0
  144. data/test/test_syntax_highlighter.rb +60 -0
  145. data/test/test_whole_file_gate.rb +175 -0
  146. data/theme/default/js/NOTICE +42 -0
  147. data/theme/default/js/run-worker.js +111 -0
  148. data/theme/default/js/run.js +350 -0
  149. data/theme/default/js/search_controller.js +134 -0
  150. data/theme/default/js/search_init.js +176 -0
  151. data/theme/default/js/search_navigation.js +110 -0
  152. data/theme/default/js/search_page.js +202 -0
  153. data/theme/default/js/search_ranker.js +244 -0
  154. data/theme/default/script.js +91 -18
  155. data/theme/default/search.css +149 -0
  156. data/theme/default/style.css +193 -21
  157. data/theme/default/syntax-highlight.css +8 -0
  158. data/theme/lillia/style.css +45 -1
  159. metadata +178 -21
  160. data/README +0 -32
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/search_index_generator.rb
4
+ #
5
+ # Builds a client-side search index compatible with RDoc's Aliki theme.
6
+ #
7
+ # The index is a flat array of entries:
8
+ #
9
+ # { name:, full_name:, type:, path: }
10
+ #
11
+ # Singleton methods and module functions (full_name holding a literal "."/
12
+ # ".#"/"?.", e.g. "File.open", "Kernel.#open", "Kernel?.open" -- see #250)
13
+ # additionally carry a +match_name+: full_name with "?." folded to ".#" and
14
+ # every "." turned into "::". This mirrors the "." -> "::" rewrite the
15
+ # vendored Aliki ranker's parseQuery() applies to the *query* text (RDoc's
16
+ # own full_names use "::" for class methods), so search_init.js/search_page.js
17
+ # can feature-detect-wrap computeScore() to compare like-for-like without
18
+ # ever touching the displayed full_name. See bitclust#279.
19
+ #
20
+ # { name:, full_name:, type:, path:, match_name: }
21
+ #
22
+ # serialized as a JavaScript assignment so it can be loaded over file://
23
+ # without tripping the browser's CORS check on JSON files:
24
+ #
25
+ # var search_data = { "index": [ ... ] };
26
+ #
27
+ # This mirrors the static layout produced by StatichtmlCommand so that the
28
+ # +path+ of each entry points at the file actually generated there.
29
+ #
30
+
31
+ require 'json'
32
+ require 'bitclust/nameutils'
33
+
34
+ module BitClust
35
+ class SearchIndexGenerator
36
+ include NameUtils
37
+
38
+ # Maps BitClust method typenames onto the type vocabulary understood by
39
+ # Aliki's searcher (class/module/constant/class_method/instance_method).
40
+ # Module functions are callable as singleton methods, so they are indexed
41
+ # as class methods; special variables have no Aliki equivalent and keep
42
+ # their own label (the searcher only uses the type for ranking).
43
+ TYPENAME_TO_SEARCH_TYPE = {
44
+ singleton_method: 'class_method',
45
+ module_function: 'class_method',
46
+ instance_method: 'instance_method',
47
+ constant: 'constant',
48
+ special_variable: 'variable',
49
+ }.freeze
50
+
51
+ # Doc pages (manual/doc/**/*.md) are prose, so language keywords such as
52
+ # "defined?", "undef" or "alias" (see rurema/doctree#2352) are never
53
+ # indexed as a class/method/library. They do, however, live at their own
54
+ # {#id}-anchored heading within a doc page (e.g. spec/def.md's "defined?"
55
+ # section). Rather than hardcoding a keyword list, scan every doc page for
56
+ # ATX headings carrying an explicit {#id} anchor -- the same anchors
57
+ # [ref:d:...] cross references already rely on -- and index each one
58
+ # individually so a keyword search can jump straight to its section.
59
+ # Headings without an explicit anchor aren't linkable, so they are
60
+ # skipped; a "# comment" that happens to open a line inside a fenced code
61
+ # block is not a heading at all, so fenced regions are skipped wholesale.
62
+ ANCHORED_HEADING_RE = /\A(\#{1,6})[ \t]+(.*?)(?:[ \t]+\{#([\w-]+)\})?[ \t]*\z/.freeze
63
+ FENCE_START_RE = /\A`{3,}/.freeze
64
+
65
+ def initialize(suffix: '.html', fs_casesensitive: false)
66
+ @suffix = suffix
67
+ @fs_casesensitive = fs_casesensitive
68
+ end
69
+
70
+ # Builds the search index as an array of entry hashes.
71
+ # +db+ is a MethodDatabase; +fdb+ is an optional FunctionDatabase (C API).
72
+ def build_index(db, fdb = nil)
73
+ index = [] #: Array[entry]
74
+ index.concat(class_entries(db))
75
+ index.concat(method_entries(db))
76
+ index.concat(library_entries(db))
77
+ index.concat(document_entries(db))
78
+ index.concat(document_heading_entries(db))
79
+ index.concat(function_entries(fdb)) if fdb
80
+ index
81
+ end
82
+
83
+ # Serializes the index as an Aliki-compatible +search_data.js+ body.
84
+ def to_js(db, fdb = nil)
85
+ "var search_data = #{JSON.generate(index: build_index(db, fdb))};"
86
+ end
87
+
88
+ # Merges per-version indexes (an array of [version, index] pairs) into a
89
+ # single index whose entries carry a +versions+ array, for the standalone
90
+ # cross-version search page. Entries are considered the same when all of
91
+ # name/full_name/type/path match. Versions are sorted numerically ("3.10"
92
+ # sorts after "3.4"), and entry order is the first appearance while
93
+ # scanning versions in ascending order — independent of the caller's
94
+ # argument order, so the generated file diffs stay stable.
95
+ #
96
+ # A module function's full_name is spelled ".#" by pre-4.0 databases and
97
+ # "?." by 4.0+ ones (bitclust#250), which would split it into two rows —
98
+ # confusing on a page that lists every version side by side. Fold the
99
+ # display to the current "?." spelling here, so both spellings collapse
100
+ # into one row with a unified versions list (name/type/path/match_name
101
+ # are spelling-independent already). Only entries carrying a match_name
102
+ # (dot-qualified methods) are folded: a prose heading whose *text*
103
+ # happens to contain ".#" must keep matching its page verbatim. The
104
+ # single-version in-page search keeps each version's own spelling —
105
+ # build_index is not affected. See the report in bitclust#279.
106
+ def self.merge(version_indexes)
107
+ merged = {} #: Hash[Array[String?], merged_entry]
108
+ sorted = version_indexes.sort_by { |version, _| Gem::Version.new(version) }
109
+ sorted.each do |version, index|
110
+ index.each do |e|
111
+ if e[:match_name] && e[:full_name].include?('.#')
112
+ e = e.merge(full_name: e[:full_name].gsub('.#', '?.'))
113
+ end
114
+ key = e.values_at(:name, :full_name, :type, :path)
115
+ entry = (merged[key] ||= e.merge(versions: []))
116
+ entry[:versions] << version
117
+ end
118
+ end
119
+ merged.values
120
+ end
121
+
122
+ # Serializes a merged multi-version index as a +search_data.js+ body.
123
+ def self.merged_js(version_indexes)
124
+ "var search_data = #{JSON.generate(index: merge(version_indexes))};"
125
+ end
126
+
127
+ private
128
+
129
+ def class_entries(db)
130
+ db.classes.sort.reject(&:dummy?).map do |c|
131
+ {
132
+ name: c.name,
133
+ full_name: c.name,
134
+ type: c.type.to_s,
135
+ path: "class/#{encode(c.name)}#{@suffix}",
136
+ }
137
+ end
138
+ end
139
+
140
+ def method_entries(db)
141
+ seen = {} #: Hash[String, bool]
142
+ result = [] #: Array[entry]
143
+ db.methods.each do |entry|
144
+ next if entry.undefined?
145
+
146
+ type = TYPENAME_TO_SEARCH_TYPE[entry.typename]
147
+ next unless type
148
+
149
+ cname = entry.klass.name
150
+ # bitclust#250: 検索結果に出る表示ラベル(name/full_name)は 4.0
151
+ # 以降のドキュメントでは module function を "?." で表示する。path
152
+ # は識別子なので typechar(常に不変)のまま
153
+ tmark = entry.display_typemark
154
+ tchar = entry.typechar
155
+ entry.names.each do |mname|
156
+ path = "method/#{encode(cname)}/#{tchar}/#{encode(mname)}#{@suffix}"
157
+ next if seen[path]
158
+
159
+ seen[path] = true
160
+ if tmark == '$'
161
+ # A special variable has no owning class to qualify it; its "$"
162
+ # sigil is the only thing distinguishing it, so keep it in +name+
163
+ # too (not just +full_name+) or a "$;"-style query can't match it.
164
+ name = full_name = tmark + mname
165
+ result << { name: name, full_name: full_name, type: type, path: path }
166
+ else
167
+ name = mname
168
+ full_name = cname + tmark + mname
169
+ item = { name: name, full_name: full_name, type: type, path: path } #: entry
170
+ if tmark.include?('.')
171
+ # bitclust#279: singleton methods (".") and module functions
172
+ # (".#"/"?.") keep a literal "." in full_name for display, but
173
+ # the vendored ranker's parseQuery() rewrites every "." in the
174
+ # *query* to "::". match_name is the same rewrite applied to
175
+ # this entry, so the search_init.js/search_page.js computeScore
176
+ # wrap can compare like-for-like. See the file header comment.
177
+ item[:match_name] = full_name.gsub('?.', '.#').gsub('.', '::')
178
+ end
179
+ result << item
180
+ end
181
+ end
182
+ end
183
+ result
184
+ end
185
+
186
+ def library_entries(db)
187
+ db.libraries.sort.map do |lib|
188
+ {
189
+ name: lib.name,
190
+ full_name: lib.name,
191
+ type: 'library',
192
+ path: "library/#{encode(lib.name)}#{@suffix}",
193
+ }
194
+ end
195
+ end
196
+
197
+ def document_entries(db)
198
+ db.docs.map do |doc|
199
+ slug = doc.name
200
+ title = doc.title
201
+ # Prose pages are identified by a Japanese title, so index the title
202
+ # (for human queries) together with the slug (for identifier queries).
203
+ label = (title && !title.empty? && title != slug) ? "#{title} (#{slug})" : slug
204
+ {
205
+ name: label,
206
+ full_name: label,
207
+ type: 'document',
208
+ path: "doc/#{encode(slug)}#{@suffix}",
209
+ }
210
+ end
211
+ end
212
+
213
+ # One entry per {#id}-anchored heading found in a doc page's body, so
214
+ # that e.g. "defined?" or "alias" (headings inside spec/def.md) surface
215
+ # directly instead of only the whole "クラス/メソッドの定義" page. See
216
+ # ANCHORED_HEADING_RE above for why this only looks at doc pages (prose,
217
+ # not method/class references) and only at explicitly anchored headings.
218
+ def document_heading_entries(db)
219
+ seen = {} #: Hash[String, bool]
220
+ result = [] #: Array[entry]
221
+ db.docs.each do |doc|
222
+ source = doc.source
223
+ next unless source
224
+ page_title = (doc.title && !doc.title.empty?) ? doc.title : doc.name
225
+ base_path = "doc/#{encode(doc.name)}#{@suffix}"
226
+ each_anchored_heading(source) do |label, id|
227
+ path = "#{base_path}##{id}"
228
+ next if seen[path]
229
+
230
+ seen[path] = true
231
+ result << {
232
+ name: label,
233
+ full_name: "#{label} (#{page_title})",
234
+ type: 'heading',
235
+ path: path,
236
+ }
237
+ end
238
+ end
239
+ result
240
+ end
241
+
242
+ # Yields [label, id] for each ATX heading in +source+ that carries an
243
+ # explicit {#id} anchor, skipping the contents of fenced code blocks
244
+ # (```lang ... ```) so an in-sample "# comment" line can never be
245
+ # mistaken for a heading.
246
+ def each_anchored_heading(source)
247
+ fence = nil #: Integer?
248
+ source.each_line do |raw|
249
+ line = raw.chomp
250
+ if fence
251
+ # Matches MDCompiler#code_fence's own terminator: the closing fence
252
+ # must be exactly as long as the opening one.
253
+ fence = nil if /\A`{#{fence}}[ \t]*\z/ =~ line
254
+ next
255
+ end
256
+ if m = FENCE_START_RE.match(line)
257
+ fence = (m[0] || raise).size
258
+ next
259
+ end
260
+ next unless m = ANCHORED_HEADING_RE.match(line)
261
+
262
+ id = m[3] or next
263
+ yield clean_heading_label(m[2] || raise), id
264
+ end
265
+ end
266
+
267
+ # Headings may carry inline Markdown (code spans, escaped punctuation);
268
+ # strip that down to plain text for the index entry.
269
+ def clean_heading_label(label)
270
+ label.gsub(/`([^`]*)`/, '\1').gsub(/\\(.)/, '\1')
271
+ end
272
+
273
+ def function_entries(fdb)
274
+ fdb.functions.sort.map do |func|
275
+ {
276
+ name: func.name,
277
+ full_name: func.name,
278
+ type: 'function',
279
+ path: "function/#{func.name}#{@suffix}",
280
+ }
281
+ end
282
+ end
283
+
284
+ def encode(str)
285
+ @fs_casesensitive ? encodename_url(str) : encodename_fs(str)
286
+ end
287
+ end
288
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  #
2
3
  # bitclust/searcher.rb
3
4
  #
@@ -39,8 +40,18 @@ module BitClust
39
40
  parser.banner = "Usage: #{@name} <pattern>"
40
41
  unless cmd == 'bitclust'
41
42
  parser.on('-d', '--database=URL', "Database location (default: #{dblocation_name()})") {|loc|
42
- url = (/:/ =~ loc) ? loc : "file://#{File.expand_path(loc)}"
43
- @dblocation = URI.parse(url)
43
+ @dblocation =
44
+ if windows_drive_path?(loc)
45
+ # "G:/Users/foo/.bitclust/db-2.2.0" のような Windows の
46
+ # ドライブレター付きパスは "/:/ =~ loc" が真になってしまい
47
+ # (":" がドライブレターの区切りに含まれるため)、既に URL
48
+ # 形式だと誤認されて file:// を付けずに URI.parse に渡って
49
+ # しまう。drive_path_uri で組み立てる
50
+ drive_path_uri(loc)
51
+ else
52
+ url = (/:/ =~ loc) ? loc : "file://#{File.expand_path(loc)}"
53
+ URI.parse(url)
54
+ end
44
55
  }
45
56
  parser.on('--server=URL', 'Spawns BitClust database server and listen URL. Requires --database option with local path.') {|url|
46
57
  require 'bitclust/server' # require here for speed
@@ -91,6 +102,14 @@ module BitClust
91
102
 
92
103
  def exec(argv, options = {})
93
104
  db = nil
105
+ prefix = options[:prefix]
106
+ if prefix
107
+ if options[:capi]
108
+ db = BitClust::FunctionDatabase.new(prefix)
109
+ else
110
+ db = BitClust::MethodDatabase.new(prefix)
111
+ end
112
+ end
94
113
  if @listen_url
95
114
  spawn_server db
96
115
  else
@@ -106,7 +125,7 @@ module BitClust
106
125
 
107
126
  def server_mode_check(argv)
108
127
  if @dblocation
109
- unless @dblocation.scheme == 'file'
128
+ unless @dblocation&.scheme == 'file'
110
129
  $stderr.puts "Give local path to --database option on server mode"
111
130
  exit 1
112
131
  end
@@ -139,19 +158,19 @@ module BitClust
139
158
  #compiler = RDCompiler::Text.new
140
159
  compiler = Plain.new
141
160
  @view = TerminalView.new(compiler,
142
- {:describe_all => @describe_all,
161
+ {:describe_all => @describe_all,
143
162
  :line => @linep,
144
163
  :encoding => @encoding})
145
164
  end
146
165
 
147
166
  def spawn_server(db)
148
- Server.new(new_local_database(db)).listen @listen_url, @foreground
167
+ Server.new(new_local_database(db)).listen (@listen_url || raise), @foreground
149
168
  end
150
169
 
151
170
  def new_local_database(db)
152
171
  return db if db
153
- path = @dblocation ? @dblocation.path : dbpath()
154
- MethodDatabase.new(path)
172
+ path = @dblocation ? @dblocation&.path : dbpath()
173
+ MethodDatabase.new(path || raise)
155
174
  end
156
175
 
157
176
  def new_database
@@ -171,15 +190,38 @@ module BitClust
171
190
 
172
191
  def find_dblocation
173
192
  %w( REFE2_SERVER BITCLUST_SERVER ).each do |key|
174
- return URI.parse(ENV[key]) if ENV[key]
193
+ return URI.parse(ENV.fetch(key)) if ENV[key]
175
194
  end
176
195
  if path = dbpath()
177
- URI.parse("file://#{path}")
196
+ # Windows で "G:/Users/foo/.bitclust/db-2.2.0" のようなドライブ
197
+ # レター付きの絶対パスを URI.parse("file://#{path}") に渡すと、
198
+ # "G" が URI のホスト部分として解釈されてしまい、path からドライブ
199
+ # レターが失われる(URI.parse("file://G:/foo").path は "/foo" に
200
+ # なってしまう)。ドライブレター付きパスのときは URI 化せず、
201
+ # drive_path_uri でドライブレターを保持したまま file: URI を作る
202
+ windows_drive_path?(path) ? drive_path_uri(path) : URI.parse("file://#{path}")
178
203
  else
179
204
  nil
180
205
  end
181
206
  end
182
207
 
208
+ # "G:/foo" や "C:\\foo" のような、Windows のドライブレターで始まる
209
+ # 絶対パスかどうかを判定する
210
+ def windows_drive_path?(path)
211
+ /\A[A-Za-z]:/.match?(path)
212
+ end
213
+
214
+ # ドライブレター付きの絶対パスを、ドライブレターを失わずに保持した
215
+ # ままの file: URI (URI::File) にする。通常の URI.parse("file://...")
216
+ # はホスト部分を認識してしまい先頭のドライブレターを取りこぼすため、
217
+ # ここでは URI::Generic#initialize の arg_check を無効にして、
218
+ # "先頭は / から始まる絶対パスであること" という通常の URI のパス
219
+ # 検証を素通りさせ、path にドライブレターを含む文字列をそのまま保持
220
+ # させる
221
+ def drive_path_uri(path)
222
+ URI::File.new('file', nil, '', nil, nil, path, nil, nil, nil, nil, false)
223
+ end
224
+
183
225
  def dbpath
184
226
  env_dbpath() || default_dbpath()
185
227
  end
@@ -187,7 +229,7 @@ module BitClust
187
229
  def env_dbpath
188
230
  [ 'REFE2_DATADIR', 'BITCLUST_DATADIR' ].each do |key|
189
231
  if ENV.key?(key)
190
- unless MethodDatabase.datadir?(ENV[key])
232
+ unless MethodDatabase.datadir?(ENV.fetch(key))
191
233
  raise InvalidDatabase, "environment variable #{key} given but #{ENV[key]} is not a valid BitClust database"
192
234
  end
193
235
  return ENV[key]
@@ -201,7 +243,7 @@ module BitClust
201
243
  [ "#{datadir}/refe2", "#{datadir}/bitclust" ].each do |path|
202
244
  return path if MethodDatabase.datadir?(path)
203
245
  end
204
- config_path = Pathname(ENV['HOME']) + ".bitclust" + "config"
246
+ config_path = Pathname(ENV.fetch('HOME')) + ".bitclust" + "config"
205
247
  if config_path.exist?
206
248
  config = YAML.load_file(config_path)
207
249
  return "#{config[:database_prefix]}-#{config[:default_version]}"
@@ -212,10 +254,14 @@ module BitClust
212
254
  def search_pattern(db, argv)
213
255
  db ||= new_database()
214
256
  @view.database ||= db if @view
257
+ # FIXME なぜか else 節にきかないのでここに書いておく
258
+ # @type var db: MethodDatabase
215
259
  case @target_type || db
216
260
  when :class
261
+ # @type var db: MethodDatabase
217
262
  find_class db, argv[0]
218
263
  when FunctionDatabase
264
+ # @type var db: FunctionDatabase
219
265
  case argv.size
220
266
  when 0
221
267
  show_all_functions db
@@ -225,6 +271,7 @@ module BitClust
225
271
  raise "must not happen: #{argv.size}"
226
272
  end
227
273
  else
274
+ # @type var db: MethodDatabase
228
275
  case argv.size
229
276
  when 0
230
277
  show_all_classes db
@@ -235,7 +282,7 @@ module BitClust
235
282
  find_method db, c, nil, m
236
283
  when 3
237
284
  c, t, m = *argv
238
- check_method_type t
285
+ check_method_type t || raise
239
286
  find_method db, c, t, m
240
287
  else
241
288
  raise "must not happen: #{argv.size}"
@@ -277,7 +324,8 @@ module BitClust
277
324
  when /\A\$/ # Special variable.
278
325
  find_method db, 'Kernel', '$', pattern.sub(/\A\$/, '')
279
326
  when /[\#,]\.|\.[\#,]|[\#\.\,]/ # method spec
280
- find_method db, *parse_method_spec_pattern(pattern)
327
+ c, t, m = parse_method_spec_pattern(pattern)
328
+ find_method db, c, t, m
281
329
  when /::/ # Class name or constant name.
282
330
  find_constant db, pattern
283
331
  when /\A[A-Z]/ # Method name or class name, but class name is better.
@@ -304,11 +352,13 @@ module BitClust
304
352
  find_method db, cnames.join('::'), '::', name
305
353
  end
306
354
 
355
+ # 区切りは反転文字列上で探す。"?." は 4.0 以降の module function の
356
+ # 表示形式(反転すると ".?")で、内部表記の ".#" に正規化する
307
357
  def parse_method_spec_pattern(pat)
308
- _m, _t, _c = pat.reverse.split(/([\#,]\.|\.[\#,]|[\#\.\,])/, 2)
309
- c = _c.reverse
310
- t = _t.tr(',', '#').sub(/\#\./, '.#')
311
- m = _m.reverse
358
+ _m, _t, _c = pat.reverse.split(/([\#,]\.|\.[\#,]|\.\?|[\#\.\,])/, 2)
359
+ c = (_c || raise).reverse
360
+ t = (_t || raise).tr(',', '#').sub(/\#\.|\.\?/, '.#')
361
+ m = (_m || raise).reverse
312
362
  return c, t, m
313
363
  end
314
364
 
@@ -402,7 +452,7 @@ module BitClust
402
452
 
403
453
  def print_packed_names(names)
404
454
  max = terminal_column()
405
- buf = ''
455
+ buf = +''
406
456
  names.each do |name|
407
457
  if buf.size + name.size + 1 > max
408
458
  if buf.empty?
@@ -410,7 +460,7 @@ module BitClust
410
460
  next
411
461
  end
412
462
  puts buf
413
- buf = ''
463
+ buf = +''
414
464
  end
415
465
  buf << name << ' '
416
466
  end
@@ -464,7 +514,7 @@ module BitClust
464
514
  puts title
465
515
  print_names methods.map{|m| m.name }
466
516
  end
467
-
517
+
468
518
  def describe_method(rec)
469
519
  unless rec.entry.library.name == '_builtin'
470
520
  puts "require '#{rec.entry.library.name}'"
@@ -500,10 +550,10 @@ module BitClust
500
550
  end
501
551
 
502
552
  def convert(string)
503
- return string if @database.nil?
553
+ return string if database.nil?
504
554
  _output_encoding = output_encoding
505
555
  return string if _output_encoding.nil?
506
- input_nkf_option = encoding_to_nkf_option(@database.encoding)
556
+ input_nkf_option = encoding_to_nkf_option(database.encoding)
507
557
  output_nkf_option = encoding_to_nkf_option(_output_encoding)
508
558
  if input_nkf_option and output_nkf_option
509
559
  NKF.nkf("-#{input_nkf_option.upcase}#{output_nkf_option}", string)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  #
2
3
  # bitclust/server.rb
3
4
  #
@@ -14,7 +15,6 @@ require 'bitclust/classentry'
14
15
  require 'bitclust/methodentry'
15
16
  require 'bitclust/docentry'
16
17
  require 'drb'
17
- require 'webrick/server'
18
18
 
19
19
  module BitClust
20
20
 
@@ -26,6 +26,11 @@ module BitClust
26
26
  end
27
27
 
28
28
  def listen(url, foreground = false)
29
+ begin
30
+ require 'webrick/server'
31
+ rescue LoadError
32
+ abort "webrick is not found. You may need to `gem install webrick` to install webrick."
33
+ end
29
34
  WEBrick::Daemon.start unless foreground
30
35
  DRb.start_service url, @db
31
36
  DRb.thread.join
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module BitClust
3
4
  # Null-object version of ProgressBar.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'bitclust/nameutils'
2
3
  require 'bitclust/methodid'
3
4
 
@@ -8,12 +9,14 @@ module BitClust
8
9
  include NameUtils
9
10
 
10
11
  module_function
11
-
12
+
12
13
  def search_pattern(db, pat)
13
14
  pat = to_pattern(pat)
14
15
  return [] if pat.empty? or /\A\s+\z/ =~ pat
15
16
  cname, type, mname = parse_method_spec_pattern(pat)
16
- cs = ms = []
17
+ # @type var cs: Array[ClassEntry]
18
+ # @type var ms: Array[MethodEntry]
19
+ cs = []; ms = []
17
20
  if cname and not cname.empty?
18
21
  if mname
19
22
  ms = find_class_method(db, cname, type, mname)
@@ -22,9 +25,9 @@ module BitClust
22
25
  cs = find_class(db, cname)
23
26
  end
24
27
  elsif type == '$'
25
- ms = find_special_vars(db, mname)
28
+ ms = find_special_vars(db, mname || raise)
26
29
  else
27
- ms = find_methods(db, mname)
30
+ ms = find_methods(db, mname || raise)
28
31
  end
29
32
  ms = ms.sort_by{|e| [e.library.name, e.klass.name] }
30
33
  cs = cs.sort_by{|e| [e.library.name] }
@@ -34,9 +37,9 @@ module BitClust
34
37
  def find_class(db, cname)
35
38
  db.classes.find_all{|c| /\b#{Regexp.escape(cname)}\w*\z/ =~ c.name }
36
39
  end
37
-
40
+
38
41
  def find_class_method(db, cname, type, mname)
39
- ret = []
42
+ ret = [] #: Array[MethodEntry]
40
43
  db.classes.each{|c|
41
44
  if /\b#{Regexp.escape(cname)}/ =~ c.name
42
45
  ret += c.methods.find_all{|m|
@@ -61,24 +64,34 @@ module BitClust
61
64
 
62
65
  def to_pattern(pat)
63
66
  pat = pat.to_str
64
- pat = pat[/\A\s*(.*?)\s*\z/, 1]
67
+ pat = pat[/\A\s*(.*?)\s*\z/, 1] || raise
65
68
  end
66
-
69
+
67
70
  def parse_method_spec_pattern(pat)
68
71
  if /\s/ =~ pat
69
72
  return parse_method_spec_pattern0(pat)
70
73
  end
71
74
  return pat, nil, nil if /\A[A-Z]\w*\z/ =~ pat
72
75
  return nil, '$', $1 if /\$(\S*)/ =~ pat
73
- _m, _t, _c = pat.reverse.split(/(::|[\#,]\.|\.[\#,]|[\#\.\,])/, 2)
76
+ # bitclust#250: "?." is an alternate spelling of the module-function
77
+ # separator ".#" (Ruby >=4.0 docs display it that way since #277; see
78
+ # NameUtils.display_typemark). Accept it here regardless of which
79
+ # Ruby version the query is about -- this is query *input*, not
80
+ # display, so both notations are always accepted. "?" is added to the
81
+ # same reversed-pattern alternative that already matches "#." and ",."
82
+ # (a marker character immediately followed by ".", read in the
83
+ # reversed pattern), so "?." matches the same way "#." already does.
84
+ _m, _t, _c = pat.reverse.split(/(::|[\#,]\.|\.[\#,\?]|[\#\.\,])/, 2)
85
+ _m || raise
74
86
  c = _c.reverse if _c
75
- t = _t.tr(',', '#').sub(/\#\./, '.#') if _t
87
+ t = _t.tr(',?', '##').sub(/\#\./, '.#') if _t
76
88
  m = _m.reverse
77
89
  return c, t, m
78
90
  end
79
91
 
80
- def parse_method_spec_pattern0(q)
81
- q = q.scan(/\S+/)[0..1]
92
+ def parse_method_spec_pattern0(pat)
93
+ # @type var q: Array[String]
94
+ q = _ = pat.scan(/\S+/)[0..1]
82
95
  q = q.reverse unless /\A[A-Z]/ =~ q[0]
83
96
  return q[0], nil, q[1]
84
97
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'pathname'
2
3
 
3
4
  require 'bitclust'
@@ -23,6 +24,12 @@ module BitClust
23
24
  @parser.parse! argv
24
25
  end
25
26
 
27
+ # グローバル --database(options[:prefix])を必要とするか。
28
+ # DB パスを自前の引数で受けるサブコマンド(searchpage)は false を返す
29
+ def needs_database?
30
+ true
31
+ end
32
+
26
33
  def help
27
34
  @parser.help
28
35
  end
@@ -56,7 +63,7 @@ module BitClust
56
63
  def align_progress_bar_title(title)
57
64
  size = title.size
58
65
  if size > 14
59
- title[0..13]
66
+ title[0..13] || raise
60
67
  else
61
68
  title + ' ' * (14 - size)
62
69
  end