bitclust-core 1.6.0 → 1.7.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
  4. data/data/bitclust/template/function-index +1 -0
  5. data/data/bitclust/template/layout +1 -0
  6. data/data/bitclust/template.offline/function-index +1 -0
  7. data/data/bitclust/template.offline/layout +3 -0
  8. data/lib/bitclust/classentry.rb +1 -1
  9. data/lib/bitclust/docentry.rb +1 -1
  10. data/lib/bitclust/functionentry.rb +1 -1
  11. data/lib/bitclust/irb.rb +68 -0
  12. data/lib/bitclust/markdown_exporter.rb +311 -0
  13. data/lib/bitclust/mdcompiler.rb +15 -1
  14. data/lib/bitclust/mdparser.rb +42 -1
  15. data/lib/bitclust/methoddatabase.rb +4 -0
  16. data/lib/bitclust/methodentry.rb +24 -0
  17. data/lib/bitclust/methodsignature.rb +1 -1
  18. data/lib/bitclust/preprocessor.rb +4 -1
  19. data/lib/bitclust/rbs_overload_matcher.rb +179 -0
  20. data/lib/bitclust/rbs_sig_importer.rb +276 -0
  21. data/lib/bitclust/rbs_signatures.rb +125 -0
  22. data/lib/bitclust/rdcompiler.rb +12 -1
  23. data/lib/bitclust/runner.rb +2 -0
  24. data/lib/bitclust/screen.rb +4 -0
  25. data/lib/bitclust/search_index_generator.rb +28 -10
  26. data/lib/bitclust/searcher.rb +12 -2
  27. data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
  28. data/lib/bitclust/subcommands/setup_command.rb +4 -1
  29. data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
  30. data/lib/bitclust/version.rb +1 -1
  31. data/lib/bitclust-irb.rb +2 -0
  32. data/test/test_irb_plugin.rb +118 -0
  33. data/test/test_link_checker.rb +1 -1
  34. data/test/test_markdown_exporter.rb +260 -0
  35. data/test/test_mdcompiler.rb +47 -5
  36. data/test/test_mdparser.rb +219 -4
  37. data/test/test_methodentry.rb +111 -0
  38. data/test/test_methodsignature.rb +3 -1
  39. data/test/test_preprocessor.rb +26 -0
  40. data/test/test_rbs_overload_matcher.rb +170 -0
  41. data/test/test_rbs_sig_importer.rb +237 -0
  42. data/test/test_rbssig_command.rb +186 -0
  43. data/test/test_rdcompiler.rb +99 -1
  44. data/test/test_search_index_generator.rb +81 -0
  45. data/test/test_setup_command.rb +21 -0
  46. data/test/test_statichtml_command.rb +137 -0
  47. data/theme/default/js/run.js +14 -0
  48. data/theme/default/js/search_init.js +3 -1
  49. data/theme/default/js/search_page.js +7 -1
  50. data/theme/default/js/version_switcher.js +147 -0
  51. data/theme/default/search.css +21 -0
  52. data/theme/default/style.css +83 -7
  53. data/theme/lillia/style.css +26 -0
  54. metadata +35 -1
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'bitclust'
4
+ require 'bitclust/methoddatabase'
5
+ require 'bitclust/subcommands/rbssig_command'
6
+ require 'stringio'
7
+ require 'tmpdir'
8
+ require 'fileutils'
9
+ require 'json'
10
+
11
+ # rbssig サブコマンド(RBS シグネチャ表示)。
12
+ #
13
+ # bitclust rbssig --sig-dir=DIR <dbpath>
14
+ # bitclust rbssig --sig-root=RBS_CHECKOUT <dbpath>
15
+ #
16
+ # RBS シグネチャは 4.0 以降のドキュメント専用なので、DB の version が
17
+ # 4.0 未満ならエラーにする(比較は display_typemark と同じく Gem::Version。
18
+ # 文字列比較だと "10.0" が "4.0" より小さく見える)。
19
+ #
20
+ # テストリスト:
21
+ # [x] グローバル --database 不要(needs_database? が false)
22
+ # [x] --sig-root/--sig-dir がどちらも無いとエラー
23
+ # [x] dbpath がちょうど 1 個でないとエラー
24
+ # [x] DB の version が 4.0 未満だとエラー
25
+ # [x] version 10.0 は 4.0 以降として通る(Gem::Version 比較)
26
+ # [x] 4.0 の DB に rbs_sig が書き込まれ、統計行が出力される
27
+ # [x] --dry-run は統計だけ表示して実 DB には書き込まない
28
+ # [x] --sig-root は core/ と stdlib/ を持つチェックアウトを読める
29
+ # [x] Runner に登録されている(bitclust rbssig として呼べる)
30
+ class TestRbssigCommand < Test::Unit::TestCase
31
+ def setup
32
+ @tmpdir = Dir.mktmpdir
33
+ @sig_dir = File.join(@tmpdir, 'sig')
34
+ FileUtils.mkdir_p(@sig_dir)
35
+ File.write(File.join(@sig_dir, 'foo.rbs'), <<~RBS)
36
+ class Foo
37
+ def bar: (Integer) -> String
38
+ end
39
+ RBS
40
+ end
41
+
42
+ def teardown
43
+ FileUtils.rm_rf(@tmpdir)
44
+ end
45
+
46
+ def test_needs_no_global_database_option
47
+ cmd = BitClust::Subcommands::RbssigCommand.new
48
+ assert_false cmd.needs_database?
49
+ end
50
+
51
+ def test_no_sig_source_is_an_error
52
+ db = build_db('4.0')
53
+ assert_command_error([db])
54
+ end
55
+
56
+ def test_requires_exactly_one_dbpath
57
+ db = build_db('4.0')
58
+ assert_command_error(["--sig-dir=#{@sig_dir}"])
59
+ assert_command_error(["--sig-dir=#{@sig_dir}", db, db])
60
+ end
61
+
62
+ def test_old_database_version_is_an_error
63
+ db = build_db('3.4')
64
+ assert_command_error(["--sig-dir=#{@sig_dir}", db])
65
+ end
66
+
67
+ def test_version_10_passes_the_gate
68
+ db = build_db('10.0')
69
+ out = run_command(["--sig-dir=#{@sig_dir}", db])
70
+ assert_match(/version 10\.0/, out)
71
+ assert_equal [['t', '('], ['c', 'Integer'], ['t', ') -> '], ['c', 'String']],
72
+ find_entry(db, 'bar').rbs_signature_overloads[0]['segments']
73
+ end
74
+
75
+ def test_fills_rbs_sig_and_prints_stats
76
+ db = build_db('4.0')
77
+ out = run_command(["--sig-dir=#{@sig_dir}", db])
78
+ assert_equal '(Integer) -> String',
79
+ find_entry(db, 'bar').rbs_signature_overloads[0]['segments'].map {|_k, t| t }.join
80
+ assert_nil find_entry(db, 'nope').rbs_signature_overloads
81
+ assert_match(
82
+ /\Adb-4\.0 \(version 4\.0\): entries_updated=1 sigs_matched=1 methods_missed=1\n\z/,
83
+ out)
84
+ end
85
+
86
+ def test_dry_run_leaves_database_untouched
87
+ db = build_db('4.0')
88
+ out = run_command(["--sig-dir=#{@sig_dir}", db, '--dry-run'])
89
+ assert_nil find_entry(db, 'bar').rbs_signature_overloads
90
+ assert_match(/entries_updated=1/, out)
91
+ end
92
+
93
+ def test_sig_root_reads_core_and_stdlib_layout
94
+ root = File.join(@tmpdir, 'rbs-checkout')
95
+ FileUtils.mkdir_p("#{root}/core")
96
+ File.write("#{root}/core/foo.rbs", <<~RBS)
97
+ class Foo
98
+ def bar: (String) -> Integer
99
+ end
100
+ RBS
101
+ # core_root を与えると rbs は stringio を暗黙に要求するので、
102
+ # 本物のチェックアウトと同じく stdlib/ も置く
103
+ FileUtils.mkdir_p("#{root}/stdlib/stringio/0")
104
+ File.write("#{root}/stdlib/stringio/0/stringio.rbs", "class StringIO\nend\n")
105
+
106
+ db = build_db('4.0')
107
+ run_command(["--sig-root=#{root}", db])
108
+ assert_equal '(String) -> Integer',
109
+ find_entry(db, 'bar').rbs_signature_overloads[0]['segments'].map {|_k, t| t }.join
110
+ end
111
+
112
+ def test_registered_in_runner
113
+ require 'bitclust/runner'
114
+ runner = BitClust::Runner.new
115
+ runner.prepare
116
+ subcommands = runner.instance_variable_get(:@subcommands)
117
+ assert_kind_of BitClust::Subcommands::RbssigCommand, subcommands['rbssig']
118
+ end
119
+
120
+ private
121
+
122
+ def build_db(version)
123
+ root = "#{@tmpdir}/tree-#{version}/refm/api/src"
124
+ FileUtils.mkdir_p("#{root}/_builtin")
125
+ File.write("#{root}/LIBRARIES", "_builtin\n")
126
+ File.write("#{root}/_builtin.rd", <<~RD)
127
+ description
128
+
129
+ = class Foo < Object
130
+ == Instance Methods
131
+ --- bar(val) -> String
132
+
133
+ 説明
134
+
135
+ --- nope -> nil
136
+
137
+ 説明
138
+ RD
139
+ prefix = "#{@tmpdir}/db-#{version}"
140
+ db = BitClust::MethodDatabase.new(prefix)
141
+ db.init
142
+ db.transaction do
143
+ db.propset('version', version)
144
+ db.propset('encoding', 'utf-8')
145
+ end
146
+ db.transaction do
147
+ db.update_by_stdlibtree(root)
148
+ end
149
+ prefix
150
+ end
151
+
152
+ def find_entry(prefix, name)
153
+ db = BitClust::MethodDatabase.new(prefix)
154
+ db.get_class('Foo').entries.find {|m| m.name?(name) }
155
+ end
156
+
157
+ def run_command(argv)
158
+ cmd = BitClust::Subcommands::RbssigCommand.new
159
+ cmd.parse(argv)
160
+ out = StringIO.new
161
+ orig_stdout = $stdout
162
+ $stdout = out
163
+ begin
164
+ cmd.exec(argv, { prefix: nil, capi: false })
165
+ ensure
166
+ $stdout = orig_stdout
167
+ end
168
+ out.string
169
+ end
170
+
171
+ def assert_command_error(argv)
172
+ cmd = BitClust::Subcommands::RbssigCommand.new
173
+ cmd.parse(argv)
174
+ assert_raise(SystemExit) do
175
+ capture_stderr { cmd.exec(argv, { prefix: nil, capi: false }) }
176
+ end
177
+ end
178
+
179
+ def capture_stderr
180
+ orig = $stderr
181
+ $stderr = StringIO.new
182
+ yield
183
+ ensure
184
+ $stderr = orig
185
+ end
186
+ end
@@ -28,9 +28,102 @@ class TestRDCompiler < Test::Unit::TestCase
28
28
  mock(method_entry).names.any_times{ names }
29
29
  mock(method_entry).since_map.any_times{ since_map }
30
30
  mock(method_entry).until_map.any_times{ until_map }
31
+ mock(method_entry).rbs_signature_overloads.any_times { nil }
31
32
  assert_equal(expected, @c.compile_method(method_entry))
32
33
  end
33
34
 
35
+ # RBS シグネチャ表示: rbs_sig property を持つエントリだけが、メソッド
36
+ # 見出しの <dt> 群の直後・説明 <dd> の直前に 1 オーバーロード 1 行の
37
+ # <dt class="rbs-signature"> を出す(dd だと説明の一部に見える。pre を
38
+ # 使うと script.js が COPY ボタンを付けてしまうので code のみ)。
39
+ # 型名セグメントは DB に存在するクラスだけリンク化し、テキストは
40
+ # エスケープした上で -> を &rarr; にする。
41
+ def test_method_with_rbs_signatures_renders_dt_lines
42
+ stub(@db).fetch_class('String') { :exists }
43
+ stub(@db).fetch_class('Integer') { raise BitClust::ClassNotFound, 'Integer' }
44
+ overloads = [
45
+ {'segments' => [['t', '('], ['c', 'Integer'], ['t', ') -> '], ['c', 'String']]},
46
+ {'segments' => [['t', '(String & Integer) -> void']]},
47
+ ]
48
+ method_entry = rbs_method_entry("--- index(val) -> Integer\n\n説明\n", overloads)
49
+
50
+ html = @c.compile_method(method_entry)
51
+ assert_include html,
52
+ %Q[(Integer) &rarr; <a href="dummy/class/String">String</a>]
53
+ assert_include html, '(String &amp; Integer) &rarr; void'
54
+ assert_match(
55
+ %r!</dt>\n(<dt class="rbs-signature"><code>.+</code></dt>\n){2}<dd class="method-description">!,
56
+ html)
57
+ assert_not_match(/<pre|<dd class="rbs-/, html)
58
+ end
59
+
60
+ # 引数パターンでチャンクが分かれるメソッド(Array.new 型)では、各
61
+ # オーバーロードが引数名・アリティ・ブロック有無で最も近いチャンクの
62
+ # 見出し直下にだけ出る(全チャンクへの重複表示をしない)
63
+ def test_method_with_rbs_signatures_assigns_overloads_to_chunks
64
+ src = <<~RD
65
+ --- index(sub) -> Integer | nil
66
+
67
+ 文字列で探す。
68
+
69
+ --- index(pattern) {|s| ... } -> Integer | nil
70
+
71
+ ブロックで探す。
72
+ RD
73
+ overloads = [
74
+ {'segments' => [['t', '(String sub) -> Integer?']],
75
+ 'params' => ['sub'], 'arity' => [1, 1]},
76
+ {'segments' => [['t', '(Regexp pattern) { (String) -> bool } -> Integer?']],
77
+ 'params' => ['pattern'], 'arity' => [1, 1], 'block' => 'req'},
78
+ ]
79
+ method_entry = rbs_method_entry(src, overloads)
80
+
81
+ html = @c.compile_method(method_entry)
82
+ assert_match(
83
+ %r!<code>index\(sub\).*?</dt>\n<dt class="rbs-signature"><code>\(String sub\) &rarr; Integer\?</code></dt>\n<dd!m,
84
+ html)
85
+ assert_match(
86
+ %r!<code>index\(pattern\).*?</dt>\n<dt class="rbs-signature"><code>\(Regexp pattern\) \{ \(String\) &rarr; bool \} &rarr; Integer\?</code></dt>\n<dd!m,
87
+ html)
88
+ assert_equal 2, html.scan('rbs-signature').size
89
+ end
90
+
91
+ # 旧形式の DB(メタ情報なし)ではチャンク振り分けができないので、
92
+ # 先頭チャンクにまとめて出す(重複はしない)
93
+ def test_method_with_rbs_signatures_without_meta_falls_back_to_first_chunk
94
+ src = <<~RD
95
+ --- index(sub) -> Integer | nil
96
+
97
+ 文字列で探す。
98
+
99
+ --- index(pattern) {|s| ... } -> Integer | nil
100
+
101
+ ブロックで探す。
102
+ RD
103
+ overloads = [
104
+ {'segments' => [['t', '(String sub) -> Integer?']]},
105
+ {'segments' => [['t', '(Regexp pattern) { (String) -> bool } -> Integer?']]},
106
+ ]
107
+ html = @c.compile_method(rbs_method_entry(src, overloads))
108
+ assert_equal 2, html.scan('rbs-signature').size
109
+ assert_match(
110
+ %r!<code>index\(sub\).*?</dt>\n(<dt class="rbs-signature">.*\n){2}<dd!,
111
+ html)
112
+ end
113
+
114
+ def rbs_method_entry(src, overloads)
115
+ method_entry = Object.new
116
+ mock(method_entry).source { src }
117
+ mock(method_entry).index_id.any_times { 'dummy' }
118
+ mock(method_entry).defined?.any_times { true }
119
+ mock(method_entry).id.any_times { 'String/i.index._builtin' }
120
+ mock(method_entry).names.any_times { [] }
121
+ mock(method_entry).since_map.any_times { {} }
122
+ mock(method_entry).until_map.any_times { {} }
123
+ mock(method_entry).rbs_signature_overloads.any_times { overloads }
124
+ method_entry
125
+ end
126
+
34
127
  # badge のカタログ訳(「Ruby %s から」等)を検証するテストで使う、
35
128
  # 日本語カタログを読み込んだ compiler を作る(setup の @c はカタログ無しの
36
129
  # C ロケールなので、_() はキーそのまま(英語)を返してしまう)
@@ -230,6 +323,7 @@ HERE
230
323
  mock(method_entry).names.any_times { ['mf'] }
231
324
  mock(method_entry).since_map.any_times { {} }
232
325
  mock(method_entry).until_map.any_times { {} }
326
+ mock(method_entry).rbs_signature_overloads.any_times { nil }
233
327
  mock(method_entry).klass.any_times { klass }
234
328
  mock(method_entry).display_typemark.any_times { '?.' }
235
329
 
@@ -250,6 +344,7 @@ HERE
250
344
  mock(method_entry).names.any_times { ['mf'] }
251
345
  mock(method_entry).since_map.any_times { {} }
252
346
  mock(method_entry).until_map.any_times { {} }
347
+ mock(method_entry).rbs_signature_overloads.any_times { nil }
253
348
 
254
349
  html = @c.compile_method(method_entry)
255
350
  assert_include(html, '<code>mf</code>')
@@ -979,7 +1074,7 @@ HERE
979
1074
 
980
1075
  data("library root" => ['[[lib:/]]', '<a href="dummy/library/">ライブラリ一覧</a>'],
981
1076
  "builtin library" => ['[[lib:_builtin]]', '<a href="dummy/library/_builtin">組み込みライブラリ</a>'],
982
- "C API root" => ['[[f:/]]', '<a href="dummy/function/">関数一覧</a>'])
1077
+ "C API root" => ['[[f:/]]', '<a href="dummy/function/">C API 関数一覧</a>'])
983
1078
  def test_bracket_link_with_catalog(data)
984
1079
  target, expected = data
985
1080
  prefix = File.expand_path('../data/bitclust/catalog', __dir__)
@@ -1029,6 +1124,7 @@ HERE
1029
1124
  mock(method_entry).names.any_times { [] }
1030
1125
  mock(method_entry).since_map.any_times { {} }
1031
1126
  mock(method_entry).until_map.any_times { {} }
1127
+ mock(method_entry).rbs_signature_overloads.any_times { nil }
1032
1128
  html = @c.compile_method(method_entry)
1033
1129
  assert_not_include(html, 'UNKNOWN_META_INFO')
1034
1130
  assert_not_include(html, '{:')
@@ -1052,6 +1148,7 @@ HERE
1052
1148
  mock(method_entry).names.any_times { [] }
1053
1149
  mock(method_entry).since_map.any_times { {} }
1054
1150
  mock(method_entry).until_map.any_times { {} }
1151
+ mock(method_entry).rbs_signature_overloads.any_times { nil }
1055
1152
  html = @c.compile_method(method_entry)
1056
1153
  assert_not_include(html, '{:')
1057
1154
  assert_include(html, 'to_a2')
@@ -1074,6 +1171,7 @@ HERE
1074
1171
  mock(method_entry).names.any_times { [] }
1075
1172
  mock(method_entry).since_map.any_times { {} }
1076
1173
  mock(method_entry).until_map.any_times { {} }
1174
+ mock(method_entry).rbs_signature_overloads.any_times { nil }
1077
1175
  html = @c.compile_method(method_entry)
1078
1176
  assert_not_include(html, '{:')
1079
1177
  assert_include(html, 'このメソッドは定義されていません。')
@@ -511,3 +511,84 @@ HERE
511
511
  end
512
512
  end
513
513
  end
514
+
515
+ class TestSearchIndexSnippet < Test::Unit::TestCase
516
+ # 検索結果の説明文(snippet)。meta description と同じ entry.description
517
+ # (最初の説明段落のプレーンテキスト)を、空でないときだけ持たせる。
518
+ #
519
+ # テストリスト:
520
+ # [x] class/method/special variable の各エントリが snippet を持つ
521
+ # [x] 説明が空のエントリは snippet キー自体を持たない
522
+ # [x] merge は複数版で同一エントリのとき最新版の snippet を採用する
523
+ # [x] 最新版に無いページは収録されている最終版の snippet を保つ
524
+ def setup
525
+ @prefix = 'db_snip'
526
+ @base = 'tree_snip'
527
+ @root = "#{@base}/refm/api/src"
528
+ FileUtils.mkdir_p("#{@root}")
529
+ File.open("#{@root}/LIBRARIES", 'w+') { |f| f.puts '_builtin' }
530
+ File.open("#{@root}/_builtin.rd", 'w+') do |f|
531
+ f.puts <<'HERE'
532
+ description
533
+
534
+ = class Foo < Object
535
+ クラスの説明。
536
+ == Instance Methods
537
+ --- foo
538
+
539
+ メソッドの説明。
540
+ --- undocumented
541
+ = module Kernel
542
+ == Special Variables
543
+ --- $; -> String | nil
544
+
545
+ 区切り文字。
546
+ HERE
547
+ end
548
+ @db = BitClust::MethodDatabase.new(@prefix)
549
+ @db.init
550
+ @db.transaction do
551
+ [%w[version 3.4], %w[encoding utf-8]].each { |k, v| @db.propset(k, v) }
552
+ end
553
+ @db.transaction { @db.update_by_stdlibtree(@root) }
554
+ @gen = BitClust::SearchIndexGenerator.new
555
+ end
556
+
557
+ def teardown
558
+ FileUtils.rm_r([@prefix, @base], :force => true)
559
+ end
560
+
561
+ def find_entry(index, full_name)
562
+ index.find { |e| e[:full_name] == full_name }
563
+ end
564
+
565
+ def test_snippet_carries_entry_description
566
+ index = @gen.build_index(@db)
567
+ assert_equal 'クラスの説明。', find_entry(index, 'Foo')[:snippet]
568
+ assert_equal 'メソッドの説明。', find_entry(index, 'Foo#foo')[:snippet]
569
+ assert_equal '区切り文字。', find_entry(index, '$;')[:snippet]
570
+ end
571
+
572
+ def test_snippet_is_omitted_when_description_is_empty
573
+ index = @gen.build_index(@db)
574
+ e = find_entry(index, 'Foo#undocumented')
575
+ assert_not_nil e
576
+ assert_false e.key?(:snippet)
577
+ end
578
+
579
+ def test_merge_takes_snippet_from_newest_version
580
+ old_e = { name: 'X', full_name: 'X', type: 'class', path: 'class/-x.html', snippet: '古い説明' }
581
+ new_e = { name: 'X', full_name: 'X', type: 'class', path: 'class/-x.html', snippet: '新しい説明' }
582
+ merged = BitClust::SearchIndexGenerator.merge([['3.4', [new_e]], ['3.3', [old_e]]])
583
+ assert_equal 1, merged.size
584
+ assert_equal '新しい説明', merged.first[:snippet]
585
+ assert_equal %w[3.3 3.4], merged.first[:versions]
586
+ end
587
+
588
+ def test_merge_keeps_snippet_of_last_version_having_the_page
589
+ only_old = { name: 'Y', full_name: 'Y', type: 'class', path: 'class/-y.html', snippet: '最終収録版の説明' }
590
+ merged = BitClust::SearchIndexGenerator.merge([['3.4', []], ['3.3', [only_old]]])
591
+ assert_equal '最終収録版の説明', merged.first[:snippet]
592
+ assert_equal %w[3.3], merged.first[:versions]
593
+ end
594
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'bitclust'
4
+ require 'bitclust/subcommands/setup_command'
5
+
6
+ # setup サブコマンドの既定バージョン。
7
+ #
8
+ # テストリスト:
9
+ # [x] 既定の対象バージョンに teeny が付いていない
10
+ # (#%if (version == "V") や #%version V の等値ゲートは文字列一致なので、
11
+ # teeny 付きの版名で DB を作ると doctree CI・生成済みドキュメントと
12
+ # ゲートの判定がずれる)
13
+ class TestSetupCommand < Test::Unit::TestCase
14
+ def test_default_versions_have_no_teeny
15
+ versions = BitClust::Subcommands::SetupCommand.new.instance_variable_get(:@versions)
16
+ assert_false(versions.empty?)
17
+ versions.each do |version|
18
+ assert_match(/\A\d+\.\d+\z/, version)
19
+ end
20
+ end
21
+ end
@@ -202,3 +202,140 @@ class TestStatichtmlSitemap < Test::Unit::TestCase
202
202
  end
203
203
  end
204
204
  end
205
+
206
+ class TestStatichtmlMarkdownOutput < Test::Unit::TestCase
207
+ def build_command(markdown_output: true)
208
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
209
+ argv = ["--fs-casesensitive"]
210
+ argv.unshift("--markdown-output") if markdown_output
211
+ cmd.parse(argv)
212
+ cmd.instance_variable_set(:@verbose, false)
213
+ cmd.send(:create_manager_config)
214
+ urlmapper = cmd.instance_variable_get(:@urlmapper)
215
+ urlmapper.bitclust_html_base = '..'
216
+ cmd.instance_variable_set(:@markdown_exporter,
217
+ BitClust::MarkdownExporter.new(urlmapper, '.html'))
218
+ cmd
219
+ end
220
+
221
+ def markdown_db
222
+ db = BitClust::MethodDatabase.dummy({ "version" => "3.4" })
223
+ def db.properties = { 'source_format' => 'markdown' }
224
+ db
225
+ end
226
+
227
+ def test_option_defaults_to_false
228
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
229
+ assert_false(cmd.instance_variable_get(:@markdown_output))
230
+ end
231
+
232
+ def test_option_is_parsed
233
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
234
+ cmd.parse(["--markdown-output"])
235
+ assert_true(cmd.instance_variable_get(:@markdown_output))
236
+ end
237
+
238
+ def test_markdown_path_replaces_html_suffix
239
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
240
+ assert_equal("out/class/ARGF.class.md",
241
+ cmd.send(:markdown_path, Pathname.new("out/class/ARGF.class.html")).to_s)
242
+ end
243
+
244
+ def test_writes_md_next_to_html_for_markdown_db
245
+ Dir.mktmpdir do |dir|
246
+ cmd = build_command
247
+ db = markdown_db
248
+ entry = BitClust::DocEntry.new(db, "help")
249
+ entry.title = "ヘルプ"
250
+ entry.source = "\n[c:String] を参照。\n"
251
+ path = Pathname.new(dir) + "doc/help.html"
252
+ FileUtils.mkdir_p(path.dirname)
253
+ cmd.send(:create_markdown_file_p, entry, path, db)
254
+ md = File.read(File.join(dir, "doc", "help.md"))
255
+ assert_equal("# ヘルプ\n\n[String](../class/String.md) を参照。\n", md)
256
+ end
257
+ end
258
+
259
+ def test_skips_md_for_non_markdown_db
260
+ Dir.mktmpdir do |dir|
261
+ cmd = build_command
262
+ db = BitClust::MethodDatabase.dummy({ "version" => "3.4" })
263
+ def db.properties = {}
264
+ entry = BitClust::DocEntry.new(db, "help")
265
+ entry.title = "ヘルプ"
266
+ entry.source = "\n本文\n"
267
+ path = Pathname.new(dir) + "doc/help.html"
268
+ FileUtils.mkdir_p(path.dirname)
269
+ cmd.send(:create_markdown_file_p, entry, path, db)
270
+ assert_false(File.exist?(File.join(dir, "doc", "help.md")))
271
+ end
272
+ end
273
+
274
+ def test_warns_when_db_is_not_markdown
275
+ cmd = build_command
276
+ db = BitClust::MethodDatabase.dummy({ "version" => "3.4" })
277
+ def db.properties = {}
278
+ orig_stderr, $stderr = $stderr, StringIO.new
279
+ begin
280
+ cmd.send(:warn_markdown_output_skipped, db, "the method database")
281
+ assert_match(/--markdown-output is ignored/, $stderr.string)
282
+ ensure
283
+ $stderr = orig_stderr
284
+ end
285
+ end
286
+
287
+ def test_no_warning_without_option
288
+ cmd = build_command(markdown_output: false)
289
+ db = BitClust::MethodDatabase.dummy({ "version" => "3.4" })
290
+ def db.properties = {}
291
+ orig_stderr, $stderr = $stderr, StringIO.new
292
+ begin
293
+ cmd.send(:warn_markdown_output_skipped, db, "the method database")
294
+ assert_equal("", $stderr.string)
295
+ ensure
296
+ $stderr = orig_stderr
297
+ end
298
+ end
299
+ end
300
+
301
+ class TestStatichtmlVersionSwitcher < Test::Unit::TestCase
302
+ def build_command(themedir, outputdir)
303
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
304
+ cmd.instance_variable_set(:@manager_config, { :themedir => Pathname.new(themedir) })
305
+ cmd.instance_variable_set(:@outputdir, Pathname.new(outputdir))
306
+ cmd.instance_variable_set(:@verbose, false)
307
+ cmd
308
+ end
309
+
310
+ def test_version_switcher_js_is_copied
311
+ Dir.mktmpdir do |dir|
312
+ themedir = File.join(dir, 'theme')
313
+ outputdir = File.join(dir, 'out')
314
+ FileUtils.mkdir_p(File.join(themedir, 'js'))
315
+ FileUtils.mkdir_p(outputdir)
316
+ File.write(File.join(themedir, 'js', 'version_switcher.js'), "// vs\n")
317
+ cmd = build_command(themedir, outputdir)
318
+ cmd.send(:copy_version_switcher_script)
319
+ assert_true(File.file?(File.join(outputdir, 'js', 'version_switcher.js')))
320
+ end
321
+ end
322
+
323
+ # An older themedir without the file must not abort the whole build.
324
+ def test_theme_without_version_switcher_js_is_tolerated
325
+ Dir.mktmpdir do |dir|
326
+ themedir = File.join(dir, 'theme')
327
+ outputdir = File.join(dir, 'out')
328
+ FileUtils.mkdir_p(themedir)
329
+ FileUtils.mkdir_p(outputdir)
330
+ cmd = build_command(themedir, outputdir)
331
+ orig_stderr, $stderr = $stderr, StringIO.new
332
+ begin
333
+ assert_nothing_raised { cmd.send(:copy_version_switcher_script) }
334
+ assert_match(/version_switcher\.js not found/, $stderr.string)
335
+ ensure
336
+ $stderr = orig_stderr
337
+ end
338
+ assert_false(File.exist?(File.join(outputdir, 'js', 'version_switcher.js')))
339
+ end
340
+ end
341
+ end
@@ -134,6 +134,20 @@ function setupBlock(pre, runner) {
134
134
  }
135
135
  group.prepend(button)
136
136
 
137
+ // RUN の使い方と制限(ruby.wasm で動かないサンプルがあること等)を
138
+ // 説明するヘルプページの RUN 節への動線。リンク先はページごとに相対
139
+ // URL が変わるため、サーバー側が layout の meta で埋め込んだ値を使う
140
+ const helpUrl = document.querySelector('meta[name="rurema-run-help"]')?.content
141
+ if (helpUrl) {
142
+ const help = document.createElement('a')
143
+ help.className = 'highlight__help-link'
144
+ help.href = helpUrl
145
+ help.textContent = '?'
146
+ help.title = 'RUN ボタンのヘルプ'
147
+ help.setAttribute('aria-label', 'RUN ボタンのヘルプ')
148
+ group.append(help)
149
+ }
150
+
137
151
  let output
138
152
  let outputTextNode
139
153
  const ensureOutput = () => {
@@ -112,7 +112,9 @@
112
112
  this.escapeHTML(typeClass) + '">' + this.formatType(r.type) + '</span>';
113
113
  }
114
114
  html += '</p>';
115
- if (r.snippet) html += '<div class="search-snippet">' + r.snippet + '</div>';
115
+ // snippet はインデックス生成側が入れたプレーンテキスト
116
+ // (エントリの説明の最初の段落)。HTML として解釈させない
117
+ if (r.snippet) html += '<div class="search-snippet">' + this.escapeHTML(r.snippet) + '</div>';
116
118
  li.innerHTML = html;
117
119
  return li;
118
120
  };
@@ -158,7 +158,13 @@
158
158
  html += '<span class="search-type search-type-' +
159
159
  this.escapeHTML(typeClass) + '">' + this.formatType(r.type) + '</span>';
160
160
  }
161
- html += '</p><p class="search-versions">';
161
+ html += '</p>';
162
+ // snippet はインデックス生成側が入れたプレーンテキスト
163
+ // (エントリの説明の最初の段落)。HTML として解釈させない
164
+ if (r.snippet) {
165
+ html += '<div class="search-snippet">' + this.escapeHTML(r.snippet) + '</div>';
166
+ }
167
+ html += '<p class="search-versions">';
162
168
  for (var i = 0; i < versions.length; i++) {
163
169
  html += '<a href="' + this.escapeHTML(versionHref(versions[i], r.path)) +
164
170
  '">' + this.escapeHTML(versions[i]) + '</a>';