bitclust-core 1.3.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/README.md +65 -0
  4. data/Rakefile +32 -0
  5. data/data/bitclust/catalog/ja_JP.UTF-8 +19 -1
  6. data/data/bitclust/searchpage/index.html +90 -0
  7. data/data/bitclust/template/class +69 -8
  8. data/data/bitclust/template/function +2 -2
  9. data/data/bitclust/template/layout +11 -3
  10. data/data/bitclust/template/method +2 -2
  11. data/data/bitclust/template/search +1 -1
  12. data/data/bitclust/template.epub/class +22 -1
  13. data/data/bitclust/template.epub/function +2 -2
  14. data/data/bitclust/template.epub/layout +1 -3
  15. data/data/bitclust/template.epub/method +2 -2
  16. data/data/bitclust/template.lillia/class +26 -5
  17. data/data/bitclust/template.lillia/layout +1 -3
  18. data/data/bitclust/template.lillia/method +2 -2
  19. data/data/bitclust/template.offline/class +48 -4
  20. data/data/bitclust/template.offline/function +2 -2
  21. data/data/bitclust/template.offline/layout +27 -3
  22. data/data/bitclust/template.offline/method +2 -2
  23. data/lib/bitclust/app.rb +16 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +67 -17
  26. data/lib/bitclust/compat.rb +2 -2
  27. data/lib/bitclust/completion.rb +58 -27
  28. data/lib/bitclust/crossrubyutils.rb +8 -5
  29. data/lib/bitclust/database.rb +28 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +1 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +47 -7
  34. data/lib/bitclust/functiondatabase.rb +13 -1
  35. data/lib/bitclust/functionentry.rb +5 -1
  36. data/lib/bitclust/functionreferenceparser.rb +4 -4
  37. data/lib/bitclust/generators/epub.rb +10 -3
  38. data/lib/bitclust/htmlutils.rb +1 -1
  39. data/lib/bitclust/include_graph.rb +375 -0
  40. data/lib/bitclust/include_pruner.rb +144 -0
  41. data/lib/bitclust/interface.rb +1 -1
  42. data/lib/bitclust/libraryentry.rb +8 -7
  43. data/lib/bitclust/lineinput.rb +7 -5
  44. data/lib/bitclust/link_checker.rb +179 -0
  45. data/lib/bitclust/markdown_bridge.rb +186 -0
  46. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  47. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  48. data/lib/bitclust/markdown_tree.rb +217 -0
  49. data/lib/bitclust/mdcompiler.rb +883 -0
  50. data/lib/bitclust/mdparser.rb +347 -0
  51. data/lib/bitclust/messagecatalog.rb +3 -3
  52. data/lib/bitclust/method_since_calculator.rb +117 -0
  53. data/lib/bitclust/methoddatabase.rb +124 -14
  54. data/lib/bitclust/methodentry.rb +125 -7
  55. data/lib/bitclust/methodid.rb +12 -11
  56. data/lib/bitclust/methodsignature.rb +10 -4
  57. data/lib/bitclust/nameutils.rb +41 -22
  58. data/lib/bitclust/preprocessor.rb +66 -29
  59. data/lib/bitclust/rdcompiler.rb +195 -95
  60. data/lib/bitclust/refsdatabase.rb +23 -8
  61. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  62. data/lib/bitclust/requesthandler.rb +21 -16
  63. data/lib/bitclust/ridatabase.rb +10 -4
  64. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  65. data/lib/bitclust/rrdparser.rb +196 -30
  66. data/lib/bitclust/runner.rb +17 -2
  67. data/lib/bitclust/screen.rb +80 -11
  68. data/lib/bitclust/search_index_generator.rb +288 -0
  69. data/lib/bitclust/searcher.rb +69 -20
  70. data/lib/bitclust/simplesearcher.rb +24 -12
  71. data/lib/bitclust/subcommand.rb +7 -1
  72. data/lib/bitclust/subcommands/ancestors_command.rb +9 -2
  73. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  74. data/lib/bitclust/subcommands/chm_command.rb +15 -7
  75. data/lib/bitclust/subcommands/classes_command.rb +5 -0
  76. data/lib/bitclust/subcommands/extract_command.rb +5 -0
  77. data/lib/bitclust/subcommands/htmlfile_command.rb +21 -7
  78. data/lib/bitclust/subcommands/init_command.rb +4 -1
  79. data/lib/bitclust/subcommands/list_command.rb +12 -4
  80. data/lib/bitclust/subcommands/lookup_command.rb +28 -9
  81. data/lib/bitclust/subcommands/methods_command.rb +10 -2
  82. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  83. data/lib/bitclust/subcommands/preproc_command.rb +5 -0
  84. data/lib/bitclust/subcommands/property_command.rb +2 -1
  85. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  86. data/lib/bitclust/subcommands/server_command.rb +10 -8
  87. data/lib/bitclust/subcommands/setup_command.rb +24 -10
  88. data/lib/bitclust/subcommands/statichtml_command.rb +129 -6
  89. data/lib/bitclust/subcommands/update_command.rb +110 -2
  90. data/lib/bitclust/syntax_highlighter.rb +48 -4
  91. data/lib/bitclust/textutils.rb +3 -3
  92. data/lib/bitclust/version.rb +1 -1
  93. data/lib/bitclust/version_badges.rb +90 -0
  94. data/lib/bitclust/whole_file_gate.rb +137 -0
  95. data/test/test_app.rb +269 -0
  96. data/test/test_bitclust.rb +2 -0
  97. data/test/test_capi_converter.rb +91 -0
  98. data/test/test_class_screen.rb +413 -0
  99. data/test/test_copy_doc_md.rb +53 -0
  100. data/test/test_doc_converter.rb +78 -0
  101. data/test/test_entity_splitter.rb +231 -0
  102. data/test/test_entry.rb +207 -2
  103. data/test/test_eol_warning.rb +43 -0
  104. data/test/test_functionreferenceparser.rb +11 -0
  105. data/test/test_include_graph.rb +640 -0
  106. data/test/test_include_pruner.rb +133 -0
  107. data/test/test_link_checker.rb +114 -0
  108. data/test/test_lookup_command.rb +135 -0
  109. data/test/test_markdown_bridge.rb +217 -0
  110. data/test/test_markdown_orchestrator.rb +478 -0
  111. data/test/test_markdown_to_rrd.rb +757 -0
  112. data/test/test_markdown_tree.rb +369 -0
  113. data/test/test_mdcompiler.rb +1276 -0
  114. data/test/test_mdparser.rb +538 -0
  115. data/test/test_method_screen.rb +166 -0
  116. data/test/test_method_since_calculator.rb +290 -0
  117. data/test/test_methoddatabase.rb +16 -0
  118. data/test/test_methodentry.rb +328 -0
  119. data/test/test_methodsignature.rb +3 -1
  120. data/test/test_methodsince_command.rb +168 -0
  121. data/test/test_nameutils.rb +22 -0
  122. data/test/test_preprocessor.rb +215 -0
  123. data/test/test_rdcompiler.rb +401 -30
  124. data/test/test_refsdatabase.rb +32 -0
  125. data/test/test_rrd_to_markdown.rb +968 -0
  126. data/test/test_rrdparser.rb +295 -2
  127. data/test/test_run_ruby_wasm.rb +54 -0
  128. data/test/test_run_worker_prelude.rb +83 -0
  129. data/test/test_runner.rb +51 -0
  130. data/test/test_search_index_generator.rb +513 -0
  131. data/test/test_search_screen.rb +48 -0
  132. data/test/test_searcher.rb +132 -0
  133. data/test/test_searchpage_command.rb +167 -0
  134. data/test/test_simplesearcher.rb +28 -0
  135. data/test/test_statichtml_command.rb +204 -0
  136. data/test/test_syntax_highlighter.rb +60 -0
  137. data/test/test_whole_file_gate.rb +175 -0
  138. data/theme/default/js/NOTICE +42 -0
  139. data/theme/default/js/run-worker.js +111 -0
  140. data/theme/default/js/run.js +350 -0
  141. data/theme/default/js/search_controller.js +134 -0
  142. data/theme/default/js/search_init.js +176 -0
  143. data/theme/default/js/search_navigation.js +110 -0
  144. data/theme/default/js/search_page.js +202 -0
  145. data/theme/default/js/search_ranker.js +244 -0
  146. data/theme/default/script.js +91 -18
  147. data/theme/default/search.css +149 -0
  148. data/theme/default/style.css +193 -21
  149. data/theme/default/syntax-highlight.css +8 -0
  150. data/theme/lillia/style.css +45 -1
  151. metadata +161 -20
  152. data/README +0 -32
@@ -0,0 +1,513 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+ require 'bitclust'
7
+ require 'bitclust/methoddatabase'
8
+ require 'bitclust/search_index_generator'
9
+
10
+ class TestSearchIndexGenerator < Test::Unit::TestCase
11
+ def setup
12
+ @prefix = 'db_si'
13
+ @base = 'tree_si'
14
+ @root = "#{@base}/refm/api/src"
15
+ setup_files
16
+ @db = BitClust::MethodDatabase.new(@prefix)
17
+ @db.init
18
+ @db.transaction do
19
+ [%w[version 3.4], %w[encoding utf-8]].each do |k, v|
20
+ @db.propset(k, v)
21
+ end
22
+ end
23
+ @db.transaction do
24
+ @db.update_by_stdlibtree(@root)
25
+ end
26
+ @gen = BitClust::SearchIndexGenerator.new
27
+ end
28
+
29
+ def teardown
30
+ FileUtils.rm_r([@prefix, @base], :force => true)
31
+ end
32
+
33
+ def find_entry(index, full_name)
34
+ index.find { |e| e[:full_name] == full_name }
35
+ end
36
+
37
+ def test_class_entry
38
+ index = @gen.build_index(@db)
39
+ e = find_entry(index, 'Foo')
40
+ assert_not_nil e
41
+ assert_equal 'Foo', e[:name]
42
+ assert_equal 'class', e[:type]
43
+ assert_equal 'class/-foo.html', e[:path]
44
+ end
45
+
46
+ def test_module_entry_type
47
+ index = @gen.build_index(@db)
48
+ e = find_entry(index, 'Kernel')
49
+ assert_not_nil e
50
+ assert_equal 'module', e[:type]
51
+ assert_equal 'class/-kernel.html', e[:path]
52
+ end
53
+
54
+ def test_instance_method_entry
55
+ index = @gen.build_index(@db)
56
+ e = find_entry(index, 'Foo#foo')
57
+ assert_not_nil e
58
+ assert_equal 'foo', e[:name]
59
+ assert_equal 'instance_method', e[:type]
60
+ assert_equal 'method/-foo/i/foo.html', e[:path]
61
+ end
62
+
63
+ def test_module_function_is_class_method
64
+ index = @gen.build_index(@db)
65
+ e = find_entry(index, 'Kernel.#at_exit')
66
+ assert_not_nil e
67
+ assert_equal 'at_exit', e[:name]
68
+ assert_equal 'class_method', e[:type]
69
+ assert_equal 'method/-kernel/m/at_exit.html', e[:path]
70
+ end
71
+
72
+ # bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を
73
+ # 独自の「.#」から「?.」に変える(表示のみ)。full_name は検索結果に
74
+ # そのまま出る表示ラベルなので変換対象だが、path(識別子)は変えない
75
+ def test_module_function_full_name_switches_to_question_dot_at_4_0
76
+ prefix = 'db_si_40'
77
+ base = 'tree_si_40'
78
+ root = "#{base}/refm/api/src"
79
+ FileUtils.mkdir_p("#{root}/_builtin")
80
+ File.write("#{root}/LIBRARIES", "_builtin\n")
81
+ File.write("#{root}/_builtin.rd", <<~RD)
82
+ description
83
+
84
+ = module Kernel
85
+ description
86
+ == Module Functions
87
+ --- at_exit{ ... } -> Proc
88
+ aaa
89
+ RD
90
+ db = BitClust::MethodDatabase.new(prefix)
91
+ db.init
92
+ db.transaction do
93
+ [%w[version 4.0], %w[encoding utf-8]].each { |k, v| db.propset(k, v) }
94
+ end
95
+ db.transaction { db.update_by_stdlibtree(root) }
96
+
97
+ index = BitClust::SearchIndexGenerator.new.build_index(db)
98
+ e = index.find { |x| x[:path] == 'method/-kernel/m/at_exit.html' }
99
+ assert_not_nil e
100
+ assert_equal 'at_exit', e[:name]
101
+ assert_equal 'class_method', e[:type]
102
+ assert_equal 'Kernel?.at_exit', e[:full_name]
103
+ ensure
104
+ FileUtils.rm_r([prefix, base], :force => true)
105
+ end
106
+
107
+ # bitclust#279: the vendored Aliki search ranker's parseQuery() rewrites
108
+ # every "." in the *query* to "::" (RDoc's own full_names use "::" for
109
+ # class methods), but BitClust keeps a literal "."/".#"/"?." in full_name
110
+ # for singleton methods and module functions. match_name mirrors that same
111
+ # rewrite on the entry side (folding "?." to ".#" first) so a
112
+ # search_init.js computeScore wrap can compare like-for-like without ever
113
+ # touching the displayed full_name. See search_index_generator.rb's header
114
+ # comment and theme/default/js/search_init.js.
115
+
116
+ def test_singleton_method_gets_match_name_with_double_colon
117
+ index = @gen.build_index(@db)
118
+ e = find_entry(index, 'Foo.bar')
119
+ assert_not_nil e
120
+ assert_equal 'class_method', e[:type]
121
+ assert_equal 'Foo::bar', e[:match_name]
122
+ end
123
+
124
+ def test_module_function_gets_match_name_with_hash_folded
125
+ index = @gen.build_index(@db)
126
+ e = find_entry(index, 'Kernel.#at_exit')
127
+ assert_not_nil e
128
+ assert_equal 'Kernel::#at_exit', e[:match_name]
129
+ end
130
+
131
+ def test_module_function_at_4_0_gets_match_name_folding_question_dot
132
+ # The "?." (4.0+) and ".#" (pre-4.0) spellings of the same module
133
+ # function must fold to the *same* match_name, so a query typed in
134
+ # either notation can find an index built from either notation
135
+ # (bitclust#250's cross-notation case).
136
+ prefix = 'db_si_40_match'
137
+ base = 'tree_si_40_match'
138
+ root = "#{base}/refm/api/src"
139
+ FileUtils.mkdir_p("#{root}/_builtin")
140
+ File.write("#{root}/LIBRARIES", "_builtin\n")
141
+ File.write("#{root}/_builtin.rd", <<~RD)
142
+ description
143
+
144
+ = module Kernel
145
+ description
146
+ == Module Functions
147
+ --- at_exit{ ... } -> Proc
148
+ aaa
149
+ RD
150
+ db = BitClust::MethodDatabase.new(prefix)
151
+ db.init
152
+ db.transaction do
153
+ [%w[version 4.0], %w[encoding utf-8]].each { |k, v| db.propset(k, v) }
154
+ end
155
+ db.transaction { db.update_by_stdlibtree(root) }
156
+
157
+ index = BitClust::SearchIndexGenerator.new.build_index(db)
158
+ e = index.find { |x| x[:path] == 'method/-kernel/m/at_exit.html' }
159
+ assert_not_nil e
160
+ assert_equal 'Kernel?.at_exit', e[:full_name]
161
+ assert_equal 'Kernel::#at_exit', e[:match_name]
162
+ ensure
163
+ FileUtils.rm_r([prefix, base], :force => true)
164
+ end
165
+
166
+ def test_instance_method_entry_has_no_match_name
167
+ index = @gen.build_index(@db)
168
+ e = find_entry(index, 'Foo#foo')
169
+ assert_not_nil e
170
+ assert_nil e[:match_name]
171
+ end
172
+
173
+ def test_constant_entry_has_no_match_name
174
+ index = @gen.build_index(@db)
175
+ e = find_entry(index, 'Foo::AAA')
176
+ assert_not_nil e
177
+ assert_nil e[:match_name]
178
+ end
179
+
180
+ def test_special_variable_entry_has_no_match_name
181
+ # Guards against a regression of issue #194: "$." already contains a
182
+ # literal "." (from the sigil-less variable name), so if it picked up a
183
+ # match_name too, it would collide with the "$"-prefix handling
184
+ # search_init.js already has (which keeps "$"-queries literal and never
185
+ # goes through the "." -> "::" rewrite at all).
186
+ index = @gen.build_index(@db)
187
+ e = find_entry(index, '$;')
188
+ assert_not_nil e
189
+ assert_nil e[:match_name]
190
+ end
191
+
192
+ def test_merge_preserves_match_name
193
+ e = { name: 'bar', full_name: 'Foo.bar', match_name: 'Foo::bar',
194
+ type: 'class_method', path: 'method/-foo/s/bar.html' }
195
+ merged = BitClust::SearchIndexGenerator.merge([['3.4', [e]]])
196
+ assert_equal 'Foo::bar', merged[0][:match_name]
197
+ end
198
+
199
+ def test_constant_entry
200
+ index = @gen.build_index(@db)
201
+ e = find_entry(index, 'Foo::AAA')
202
+ assert_not_nil e
203
+ assert_equal 'AAA', e[:name]
204
+ assert_equal 'constant', e[:type]
205
+ assert_equal 'method/-foo/c/-a-a-a.html', e[:path]
206
+ end
207
+
208
+ def test_special_variable_keeps_its_sigil_in_name
209
+ # A special variable's "$" is part of how it is written and is the only
210
+ # thing distinguishing it (there is no owning class to qualify it), so it
211
+ # must stay in +name+ for a "$;"-style query to match. See issue #194.
212
+ #
213
+ # The +name+/+type+ assertions below are what issue #194 is about. The
214
+ # +path+ assertion is secondary and filesystem-dependent: @gen defaults to
215
+ # fs_casesensitive: false, so the path is built by encodename_fs ("Kernel"
216
+ # -> "-kernel", ";" -> "=3b"). It is fine to ignore ONLY the path failure if
217
+ # it broke because the fs encoding scheme changed or a case-sensitive build
218
+ # (fs_casesensitive: true, giving "method/Kernel/v/...") is in use -- as long
219
+ # as name/type still hold. A name/type failure is a real regression.
220
+ index = @gen.build_index(@db)
221
+ e = find_entry(index, '$;')
222
+ assert_not_nil e
223
+ assert_equal '$;', e[:name]
224
+ assert_equal 'variable', e[:type]
225
+ assert_equal 'method/-kernel/v/=3b.html', e[:path],
226
+ 'filesystem-encoded path (fs_casesensitive: false); ' \
227
+ 'ignore only this failure if just the fs encoding/casing changed'
228
+ end
229
+
230
+ def test_special_variable_word_name_keeps_its_sigil
231
+ index = @gen.build_index(@db)
232
+ e = find_entry(index, '$stdout')
233
+ assert_not_nil e
234
+ assert_equal '$stdout', e[:name]
235
+ assert_equal 'variable', e[:type]
236
+ end
237
+
238
+ def test_same_method_name_in_two_classes
239
+ index = @gen.build_index(@db)
240
+ assert_not_nil find_entry(index, 'Foo#foo')
241
+ assert_not_nil find_entry(index, 'Bar#foo')
242
+ end
243
+
244
+ def test_no_duplicate_paths
245
+ index = @gen.build_index(@db)
246
+ paths = index.map { |e| e[:path] }
247
+ assert_equal paths.uniq.size, paths.size, 'index must not contain duplicate paths'
248
+ end
249
+
250
+ def test_document_entry_uses_title_and_slug
251
+ index = @gen.build_index(@db)
252
+ e = index.find { |x| x[:type] == 'document' && x[:path] == 'doc/glossary.html' }
253
+ assert_not_nil e
254
+ assert_equal 'Ruby用語集 (glossary)', e[:full_name]
255
+ assert_equal 'Ruby用語集 (glossary)', e[:name]
256
+ end
257
+
258
+ # {#id}-anchored doc headings (rurema/doctree#2352: keywords like
259
+ # defined?/undef/alias aren't methods, so they never show up in the
260
+ # class/method-derived index -- they only show up via their doc page
261
+ # heading). These need a native md doc tree (copy_doc_md), which is a
262
+ # different ingestion path than the RD @root fixture set up in #setup, so
263
+ # they build their own tiny db per case (mirrors test_copy_doc_md.rb).
264
+
265
+ def build_md_doc_index(files)
266
+ Dir.mktmpdir do |dir|
267
+ files.each do |relpath, content|
268
+ path = File.join(dir, 'manual', 'doc', relpath)
269
+ FileUtils.mkdir_p(File.dirname(path))
270
+ File.write(path, content)
271
+ end
272
+ FileUtils.mkdir_p(File.join(dir, 'manual', 'api'))
273
+
274
+ prefix = File.join(dir, 'db')
275
+ db = BitClust::MethodDatabase.new(prefix)
276
+ db.init
277
+ db.transaction do
278
+ db.propset('version', '3.4')
279
+ db.propset('encoding', 'utf-8')
280
+ end
281
+ db.transaction do
282
+ db.instance_variable_set(:@md_root, File.join(dir, 'manual', 'api'))
283
+ db.__send__(:copy_doc_md)
284
+ end
285
+ # Reopen (like test_copy_doc_md.rb) rather than reuse the writer db,
286
+ # so the index is built from what was actually persisted.
287
+ db2 = BitClust::MethodDatabase.new(prefix)
288
+ BitClust::SearchIndexGenerator.new.build_index(db2)
289
+ end
290
+ end
291
+
292
+ def test_document_heading_entry_for_anchored_heading
293
+ index = build_md_doc_index(
294
+ 'spec/def.md' => <<~MD
295
+ # クラス/メソッドの定義
296
+
297
+ 本文。
298
+
299
+ #### alias {#alias}
300
+
301
+ alias の説明。
302
+
303
+ #### undef {#undef}
304
+
305
+ undef の説明。
306
+ MD
307
+ )
308
+ e = index.find { |x| x[:type] == 'heading' && x[:name] == 'alias' }
309
+ assert_not_nil e
310
+ assert_equal 'alias (クラス/メソッドの定義)', e[:full_name]
311
+ assert_equal 'doc/spec=2fdef.html#alias', e[:path]
312
+
313
+ e2 = index.find { |x| x[:type] == 'heading' && x[:name] == 'undef' }
314
+ assert_not_nil e2
315
+ assert_equal 'doc/spec=2fdef.html#undef', e2[:path]
316
+ end
317
+
318
+ def test_heading_without_anchor_is_not_indexed
319
+ index = build_md_doc_index(
320
+ 'spec/def.md' => <<~MD
321
+ # クラス/メソッドの定義
322
+
323
+ #### 見出し
324
+
325
+ 本文。
326
+ MD
327
+ )
328
+ assert_nil index.find { |x| x[:type] == 'heading' }
329
+ end
330
+
331
+ def test_heading_inside_fenced_code_is_not_mistaken_for_a_heading
332
+ # A "# ..." line at column 0 inside a ```ruby sample must not be
333
+ # misdetected as an h1 just because it starts with "#".
334
+ index = build_md_doc_index(
335
+ 'spec/control.md' => <<~MD
336
+ # 制御構造
337
+
338
+ #### if {#if}
339
+
340
+ ```ruby
341
+ # this is not a heading
342
+ if true
343
+ 1
344
+ end
345
+ ```
346
+ MD
347
+ )
348
+ headings = index.select { |x| x[:type] == 'heading' }
349
+ assert_equal 1, headings.size
350
+ assert_equal 'if', headings[0][:name]
351
+ end
352
+
353
+ def test_heading_label_strips_inline_code_span_backticks
354
+ index = build_md_doc_index(
355
+ 'spec/comment.md' => <<~MD
356
+ # コメント
357
+
358
+ ### 文字列リテラルの凍結(`frozen_string_literal`) {#frozen_string_literal}
359
+
360
+ 本文。
361
+ MD
362
+ )
363
+ e = index.find { |x| x[:type] == 'heading' }
364
+ assert_not_nil e
365
+ assert_equal '文字列リテラルの凍結(frozen_string_literal)', e[:name]
366
+ end
367
+
368
+ def test_to_js_format
369
+ js = @gen.to_js(@db)
370
+ assert_match(/\Avar search_data = \{/, js)
371
+ assert(js.end_with?(';'), 'to_js output must end with a semicolon')
372
+
373
+ json = js.sub(/\Avar search_data = /, '').sub(/;\z/, '')
374
+ data = JSON.parse(json)
375
+ assert_kind_of Array, data['index']
376
+ assert(data['index'].any? { |e| e['full_name'] == 'Foo#foo' })
377
+ end
378
+
379
+ # 全バージョン対応の検索ページ用: 版ごとの index を versions タグ付きで統合する
380
+
381
+ def entry(over = {})
382
+ { name: 'Foo', full_name: 'Foo', type: 'class', path: 'class/-foo.html' }.merge(over)
383
+ end
384
+
385
+ def test_merge_dedupes_identical_entries_across_versions
386
+ merged = BitClust::SearchIndexGenerator.merge(
387
+ [['3.4', [entry]], ['3.0', [entry]]])
388
+ assert_equal 1, merged.size
389
+ assert_equal %w[3.0 3.4], merged[0][:versions]
390
+ assert_equal 'Foo', merged[0][:full_name]
391
+ end
392
+
393
+ def test_merge_sorts_versions_numerically
394
+ # "3.10" は文字列比較だと "3.4" より前に来てしまう
395
+ merged = BitClust::SearchIndexGenerator.merge(
396
+ [['4.1', [entry]], ['3.10', [entry]], ['3.4', [entry]]])
397
+ assert_equal %w[3.4 3.10 4.1], merged[0][:versions]
398
+ end
399
+
400
+ def test_merge_keeps_entries_with_different_paths_separate
401
+ a = entry(full_name: 'X#x', name: 'x', type: 'instance_method',
402
+ path: 'method/-x/i/x.html')
403
+ b = a.merge(path: 'method/=x/i/x.html')
404
+ merged = BitClust::SearchIndexGenerator.merge([['3.4', [a]], ['4.1', [b]]])
405
+ assert_equal 2, merged.size
406
+ assert_equal [['3.4'], ['4.1']], merged.map { |e| e[:versions] }
407
+ end
408
+
409
+ def test_merge_orders_entries_by_first_appearance_in_version_order
410
+ # 入力順に依らず「昇順の版を走査して最初に現れた順」で安定させる
411
+ # (generated-documents にコミットされる出力の diff を安定させるため)
412
+ a = entry(full_name: 'A', name: 'A', path: 'class/-a.html')
413
+ b = entry(full_name: 'B', name: 'B', path: 'class/-b.html')
414
+ merged = BitClust::SearchIndexGenerator.merge(
415
+ [['3.4', [a, b]], ['3.0', [b]]])
416
+ assert_equal %w[B A], merged.map { |e| e[:full_name] }
417
+ end
418
+
419
+ def test_merge_folds_module_function_display_to_question_dot
420
+ # bitclust#279 コメント対応: 統合ページ(/ja/search/)で同じ module
421
+ # function が "Kernel.#open"(4.0 より前の版)と "Kernel?.open"
422
+ # (4.0 以降)の2行に分かれて出るのはわかりにくい。表示は現行表記の
423
+ # "?." に畳んで1エントリに合流させる(name/type/path/match_name は
424
+ # 表記に依らず同一なので、full_name さえ畳めばキーが一致する)。
425
+ # 単一版ページの表示はその版の表記のまま(build_index 側は触らない)。
426
+ pre40 = { name: 'open', full_name: 'Kernel.#open', type: 'class_method',
427
+ path: 'method/-kernel/m/open.html', match_name: 'Kernel::#open' }
428
+ post40 = { name: 'open', full_name: 'Kernel?.open', type: 'class_method',
429
+ path: 'method/-kernel/m/open.html', match_name: 'Kernel::#open' }
430
+ merged = BitClust::SearchIndexGenerator.merge(
431
+ [['3.4', [pre40]], ['4.0', [post40]]])
432
+ assert_equal 1, merged.size
433
+ assert_equal 'Kernel?.open', merged[0][:full_name]
434
+ assert_equal %w[3.4 4.0], merged[0][:versions]
435
+ assert_equal 'Kernel::#open', merged[0][:match_name]
436
+ end
437
+
438
+ def test_merge_folds_display_even_when_only_old_notation_exists
439
+ # 4.0 以降に存在しないまま消えたメソッドでも、統合ページの表示は
440
+ # "?." に揃える(znz さんの依頼は「?. だけ出てくるように」)
441
+ pre40 = { name: 'gone', full_name: 'Kernel.#gone', type: 'class_method',
442
+ path: 'method/-kernel/m/gone.html', match_name: 'Kernel::#gone' }
443
+ merged = BitClust::SearchIndexGenerator.merge([['3.4', [pre40]]])
444
+ assert_equal 'Kernel?.gone', merged[0][:full_name]
445
+ end
446
+
447
+ def test_merge_keeps_dot_hash_in_entries_without_match_name
448
+ # ".#" を字面に含んでいても match_name を持たないエントリ(散文
449
+ # 見出しなど)は表示を畳まない — 見出しはページ本文の字面と一致
450
+ # していることに意味がある
451
+ heading = { name: '.#', full_name: '.# (プログラム・文・式)', type: 'heading',
452
+ path: 'doc/symref.html#dot-hash' }
453
+ merged = BitClust::SearchIndexGenerator.merge([['3.4', [heading]]])
454
+ assert_equal '.# (プログラム・文・式)', merged[0][:full_name]
455
+ end
456
+
457
+ def test_merged_js_format
458
+ js = BitClust::SearchIndexGenerator.merged_js([['3.4', [entry]]])
459
+ assert_match(/\Avar search_data = \{/, js)
460
+ assert(js.end_with?(';'))
461
+ data = JSON.parse(js.sub(/\Avar search_data = /, '').sub(/;\z/, ''))
462
+ assert_equal ['3.4'], data['index'][0]['versions']
463
+ assert_equal 'Foo', data['index'][0]['full_name']
464
+ end
465
+
466
+ private
467
+
468
+ def setup_files
469
+ FileUtils.mkdir_p("#{@root}/_builtin")
470
+
471
+ File.open("#{@root}/LIBRARIES", 'w+') do |file|
472
+ file.puts '_builtin'
473
+ end
474
+
475
+ File.open("#{@root}/_builtin.rd", 'w+') do |file|
476
+ file.puts <<'HERE'
477
+ description
478
+
479
+ = class Foo < Object
480
+ description
481
+ == Singleton Methods
482
+ --- bar
483
+ == Instance Methods
484
+ --- foo
485
+ == Constants
486
+ --- AAA
487
+ = class Bar < Object
488
+ == Instance Methods
489
+ --- foo
490
+ = module Kernel
491
+ description
492
+ == Module Functions
493
+ --- at_exit{ ... } -> Proc
494
+ aaa
495
+ == Special Variables
496
+ --- $; -> String | nil
497
+ 区切り文字。
498
+ --- $stdout -> IO
499
+ 標準出力。
500
+
501
+ HERE
502
+ end
503
+
504
+ # Prose doc pages are loaded from <stdlibtree>/../../doc/**/*.rd
505
+ docdir = "#{@base}/refm/doc"
506
+ FileUtils.mkdir_p(docdir)
507
+ File.open("#{docdir}/glossary.rd", 'w+') do |file|
508
+ file.puts '= Ruby用語集'
509
+ file.puts
510
+ file.puts '用語の説明。'
511
+ end
512
+ end
513
+ end
@@ -0,0 +1,48 @@
1
+ require 'test/unit'
2
+ require 'bitclust'
3
+ require 'bitclust/screen'
4
+
5
+ # Covers data/bitclust/template/search: the dynamic CGI search-results page
6
+ # rendered by `bitclust server`'s /search endpoint (SearchScreen). Distinct
7
+ # from the static client-side search index (SearchIndexGenerator, covered by
8
+ # test_search_index_generator.rb) -- this one builds its "Kernel.#mf"-style
9
+ # label directly in the template from klass.name + typemark + signature.
10
+ #
11
+ # bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を
12
+ # 独自の「.#」から「?.」に変える(表示のみ、識別子は変えない)。
13
+ class TestSearchScreenModuleFunctionDisplay < Test::Unit::TestCase
14
+ SRC = <<'HERE'
15
+ = module Kernel
16
+ description
17
+ == Module Functions
18
+ --- mf
19
+
20
+ 説明
21
+ HERE
22
+
23
+ def render(version)
24
+ _lib, db = BitClust::RRDParser.parse(SRC, 'testlib', {'version' => version})
25
+ entry = db.get_method(BitClust::MethodSpec.parse('Kernel.#mf'))
26
+ datadir = File.expand_path('../data/bitclust', __dir__)
27
+ manager = BitClust::ScreenManager.new(
28
+ :templatedir => "#{datadir}/template",
29
+ :catalogdir => "#{datadir}/catalog",
30
+ :encoding => 'utf-8',
31
+ :default_encoding => 'utf-8',
32
+ :base_url => '',
33
+ :target_version => version
34
+ )
35
+ manager.search_screen([entry], :database => db, :q => 'mf', :elapsed_time => 0.0).body
36
+ end
37
+
38
+ def test_module_function_shows_dot_hash_before_4_0
39
+ html = render('3.4')
40
+ assert_include(html, 'Kernel.#mf')
41
+ end
42
+
43
+ def test_module_function_switches_to_question_dot_at_4_0
44
+ html = render('4.0')
45
+ assert_include(html, 'Kernel?.mf')
46
+ assert_not_include(html, 'Kernel.#mf')
47
+ end
48
+ end
@@ -1,6 +1,8 @@
1
1
  require "test/unit"
2
2
  require "bitclust"
3
3
  require "bitclust/searcher"
4
+ require "fileutils"
5
+ require "tmpdir"
4
6
 
5
7
  class TestTerminalView < Test::Unit::TestCase
6
8
  include BitClust
@@ -19,3 +21,133 @@ class TestTerminalView < Test::Unit::TestCase
19
21
  assert_empty err
20
22
  end
21
23
  end
24
+
25
+ class TestSearcherWindowsDrivePath < Test::Unit::TestCase
26
+ include BitClust
27
+
28
+ ENV_KEYS = %w[REFE2_SERVER BITCLUST_SERVER REFE2_DATADIR BITCLUST_DATADIR]
29
+
30
+ def setup
31
+ @saved_env = {}
32
+ ENV_KEYS.each {|key| @saved_env[key] = ENV.delete(key) }
33
+ @saved_dir = Dir.pwd
34
+ @searcher = Searcher.new
35
+ end
36
+
37
+ def teardown
38
+ Dir.chdir(@saved_dir)
39
+ ENV_KEYS.each do |key|
40
+ if @saved_env[key]
41
+ ENV[key] = @saved_env[key]
42
+ else
43
+ ENV.delete(key)
44
+ end
45
+ end
46
+ end
47
+
48
+ def test_windows_drive_path_predicate
49
+ assert_true @searcher.send(:windows_drive_path?, "G:/Users/foo/.bitclust/db-2.2.0")
50
+ assert_true @searcher.send(:windows_drive_path?, 'C:\Users\foo\.bitclust\db-2.2.0')
51
+ assert_false @searcher.send(:windows_drive_path?, "/home/foo/.bitclust/db-2.2.0")
52
+ assert_false @searcher.send(:windows_drive_path?, "druby://localhost:10001")
53
+ end
54
+
55
+ def test_drive_path_uri_keeps_drive_letter
56
+ uri = @searcher.send(:drive_path_uri, "G:/Users/foo/.bitclust/db-2.2.0")
57
+ assert_equal "file", uri.scheme
58
+ assert_equal "G:/Users/foo/.bitclust/db-2.2.0", uri.path
59
+ end
60
+
61
+ def test_find_dblocation_keeps_windows_drive_letter
62
+ with_fake_datadir("G:/Users/foo/.bitclust/db-2.2.0") do |relative_path|
63
+ ENV["BITCLUST_DATADIR"] = relative_path
64
+ location = @searcher.send(:find_dblocation)
65
+ assert_equal "file", location.scheme
66
+ assert_equal relative_path, location.path
67
+ end
68
+ end
69
+
70
+ def test_find_dblocation_keeps_unix_absolute_path
71
+ Dir.mktmpdir do |dir|
72
+ datadir = File.join(dir, ".bitclust", "db-2.2.0")
73
+ FileUtils.mkdir_p(datadir)
74
+ FileUtils.touch(File.join(datadir, "properties"))
75
+ ENV["BITCLUST_DATADIR"] = datadir
76
+ location = @searcher.send(:find_dblocation)
77
+ assert_equal "file", location.scheme
78
+ assert_equal datadir, location.path
79
+ end
80
+ end
81
+
82
+ def test_database_option_keeps_windows_drive_letter
83
+ @searcher.parser.parse!(["--database=G:/Users/foo/.bitclust/db-2.2.0"])
84
+ dblocation = @searcher.instance_variable_get(:@dblocation)
85
+ assert_equal "file", dblocation.scheme
86
+ assert_equal "G:/Users/foo/.bitclust/db-2.2.0", dblocation.path
87
+ end
88
+
89
+ private
90
+
91
+ # datadir/properties を tmpdir 内に作り、tmpdir へ chdir したうえで
92
+ # datadir までの相対パス(ドライブレターを含む)を渡す。":" は Unix の
93
+ # ファイル名として有効な文字なので、Linux 上でも "G:" というディレク
94
+ # トリを作ってドライブレター付きパスの再現ができる
95
+ def with_fake_datadir(relative_path)
96
+ Dir.mktmpdir do |dir|
97
+ datadir = File.join(dir, *relative_path.split("/"))
98
+ FileUtils.mkdir_p(datadir)
99
+ FileUtils.touch(File.join(datadir, "properties"))
100
+ Dir.chdir(dir) do
101
+ yield relative_path
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ # refe の module_function 表示(bitclust#297)。
108
+ # HTML 側(#282)と同じく、DB のバージョンが 4.0 以降なら
109
+ # "Foo.#baz" ではなく "Foo?.baz" と表示する。
110
+ class TestSearcherModuleFunctionDisplay < Test::Unit::TestCase
111
+ include BitClust
112
+
113
+ def record_for(version)
114
+ db = MethodDatabase.dummy("version" => version)
115
+ spec = MethodSpec.parse("Foo.#baz")
116
+ SearchResult::Record.new(db, spec, spec)
117
+ end
118
+
119
+ def test_names_folds_module_function_typemark_for_40
120
+ assert_equal ["Foo?.baz"], record_for("4.0").names
121
+ end
122
+
123
+ def test_names_keeps_typemark_before_40
124
+ assert_equal ["Foo.#baz"], record_for("3.4").names
125
+ end
126
+
127
+ def test_names_without_db_version_keeps_typemark
128
+ spec = MethodSpec.parse("Foo.#baz")
129
+ assert_equal ["Foo.#baz"], SearchResult::Record.new(nil, spec, spec).names
130
+ end
131
+ end
132
+
133
+ # refe のクエリ側も 4.0 以降の表示形式 "Foo?.baz" を受け付ける(bitclust#297)
134
+ class TestSearcherModuleFunctionQuery < Test::Unit::TestCase
135
+ include BitClust
136
+
137
+ def parse(pat)
138
+ Searcher.new.send(:parse_method_spec_pattern, pat)
139
+ end
140
+
141
+ def test_question_dot_pattern
142
+ assert_equal ["Foo", ".#", "baz"], parse("Foo?.baz")
143
+ end
144
+
145
+ def test_dot_hash_pattern_still_works
146
+ assert_equal ["Foo", ".#", "baz"], parse("Foo.#baz")
147
+ end
148
+
149
+ def test_predicate_method_names_unaffected
150
+ assert_equal ["Foo", "#", "empty?"], parse("Foo#empty?")
151
+ assert_equal ["Foo", ".", "b?"], parse("Foo.b?")
152
+ end
153
+ end