bitclust-core 1.6.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) 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/class-index +0 -1
  5. data/data/bitclust/template/function-index +1 -0
  6. data/data/bitclust/template/layout +1 -0
  7. data/data/bitclust/template.epub/class +5 -5
  8. data/data/bitclust/template.epub/class-index +0 -1
  9. data/data/bitclust/template.epub/contents +7 -4
  10. data/data/bitclust/template.epub/layout +4 -4
  11. data/data/bitclust/template.offline/class-index +0 -1
  12. data/data/bitclust/template.offline/function-index +1 -0
  13. data/data/bitclust/template.offline/layout +3 -0
  14. data/lib/bitclust/classentry.rb +1 -1
  15. data/lib/bitclust/docentry.rb +1 -1
  16. data/lib/bitclust/functionentry.rb +1 -1
  17. data/lib/bitclust/generators/epub.rb +69 -5
  18. data/lib/bitclust/irb.rb +74 -0
  19. data/lib/bitclust/markdown_bridge.rb +11 -6
  20. data/lib/bitclust/markdown_exporter.rb +311 -0
  21. data/lib/bitclust/markdown_tree.rb +118 -11
  22. data/lib/bitclust/mdcompiler.rb +15 -1
  23. data/lib/bitclust/mdparser.rb +42 -1
  24. data/lib/bitclust/methoddatabase.rb +17 -4
  25. data/lib/bitclust/methodentry.rb +24 -0
  26. data/lib/bitclust/methodsignature.rb +1 -1
  27. data/lib/bitclust/nameutils.rb +13 -0
  28. data/lib/bitclust/preprocessor.rb +4 -37
  29. data/lib/bitclust/rbs_overload_matcher.rb +179 -0
  30. data/lib/bitclust/rbs_sig_importer.rb +276 -0
  31. data/lib/bitclust/rbs_signatures.rb +125 -0
  32. data/lib/bitclust/rdcompiler.rb +12 -1
  33. data/lib/bitclust/remote.rb +405 -0
  34. data/lib/bitclust/runner.rb +7 -10
  35. data/lib/bitclust/screen.rb +6 -1
  36. data/lib/bitclust/search_index_generator.rb +28 -10
  37. data/lib/bitclust/searcher.rb +54 -25
  38. data/lib/bitclust/subcommands/chm_command.rb +1 -1
  39. data/lib/bitclust/subcommands/epub_command.rb +2 -2
  40. data/lib/bitclust/subcommands/extract_command.rb +7 -0
  41. data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
  42. data/lib/bitclust/subcommands/methods_command.rb +5 -28
  43. data/lib/bitclust/subcommands/query_command.rb +1 -0
  44. data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
  45. data/lib/bitclust/subcommands/server_command.rb +21 -10
  46. data/lib/bitclust/subcommands/setup_command.rb +32 -11
  47. data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
  48. data/lib/bitclust/syntax_highlighter.rb +65 -4
  49. data/lib/bitclust/user_dirs.rb +62 -0
  50. data/lib/bitclust/version.rb +1 -1
  51. data/lib/bitclust-irb.rb +2 -0
  52. data/test/test_chm_command.rb +22 -0
  53. data/test/test_epub_command.rb +60 -0
  54. data/test/test_epub_generator.rb +330 -0
  55. data/test/test_extract_command.rb +45 -0
  56. data/test/test_irb_plugin.rb +212 -0
  57. data/test/test_link_checker.rb +1 -1
  58. data/test/test_markdown_bridge.rb +35 -0
  59. data/test/test_markdown_exporter.rb +260 -0
  60. data/test/test_markdown_tree.rb +144 -0
  61. data/test/test_mdcompiler.rb +47 -5
  62. data/test/test_mdparser.rb +223 -4
  63. data/test/test_methodentry.rb +111 -0
  64. data/test/test_methods_command.rb +32 -0
  65. data/test/test_methodsignature.rb +3 -1
  66. data/test/test_opensearchdescription_screen.rb +39 -0
  67. data/test/test_preprocessor.rb +26 -0
  68. data/test/test_query_command.rb +30 -0
  69. data/test/test_rbs_overload_matcher.rb +170 -0
  70. data/test/test_rbs_sig_importer.rb +237 -0
  71. data/test/test_rbssig_command.rb +186 -0
  72. data/test/test_rdcompiler.rb +99 -1
  73. data/test/test_remote.rb +374 -0
  74. data/test/test_runner.rb +61 -1
  75. data/test/test_search_index_generator.rb +81 -0
  76. data/test/test_searcher.rb +63 -0
  77. data/test/test_server_command.rb +41 -0
  78. data/test/test_setup_command.rb +164 -0
  79. data/test/test_statichtml_command.rb +137 -0
  80. data/test/test_syntax_highlighter.rb +124 -0
  81. data/test/test_user_dirs.rb +62 -0
  82. data/theme/default/js/run.js +14 -0
  83. data/theme/default/js/search_init.js +3 -1
  84. data/theme/default/js/search_page.js +7 -1
  85. data/theme/default/js/version_switcher.js +147 -0
  86. data/theme/default/search.css +21 -0
  87. data/theme/default/style.css +83 -7
  88. data/theme/lillia/style.css +26 -0
  89. metadata +71 -2
  90. data/lib/bitclust/ridatabase.rb +0 -359
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'stringio'
6
+ require 'json'
7
+ require 'bitclust'
8
+ require 'bitclust/methoddatabase'
9
+ require 'bitclust/searcher'
10
+ require 'bitclust/irb'
11
+
12
+ # bitclust-irb gem の irb コマンド(refe)。リモート検索そのものは
13
+ # test_remote.rb で検証する。
14
+ #
15
+ # テストリスト:
16
+ # [x] TerminalView が io: に出力する(既定は従来どおり $stdout)
17
+ # [x] Irb.lookup がメソッドを検索して整形結果を io へ書く
18
+ # [x] Irb.lookup がクラスも引ける
19
+ # [x] 空のパターンは使い方を表示して例外にしない
20
+ # [x] 見つからないパターンはメッセージを io へ書いて例外にしない
21
+ # [x] require で IRB::Command::Base のサブクラスとして登録される
22
+ # [x] Searcher#local_database? は既定の場所に DB が無ければ false・あれば true
23
+ # [x] Irb.lookup: db が与えられていれば remote を使わない
24
+ # [x] Irb.lookup: ローカル DB が無ければ remote で検索する
25
+ # [x] Irb.lookup: remote が nil なら bitclust setup を案内する
26
+ # [x] refe の CLI(Searcher#exec)も DB が無ければ remote で検索する
27
+ class TestBitClustIrbPlugin < Test::Unit::TestCase
28
+ FOO_MD = <<~'MD'
29
+ ---
30
+ library: foo
31
+ ---
32
+ # class Foo < Object
33
+
34
+ クラスの説明。
35
+
36
+ ## Instance Methods
37
+
38
+ ### def bar -> nil
39
+
40
+ bar の説明。
41
+ MD
42
+
43
+ BASE_URL = 'https://docs.ruby-lang.org/ja/latest/'
44
+ INDEX_JS = "var search_data = #{JSON.generate(index: [
45
+ { name: 'String', full_name: 'String', type: 'class', path: 'class/String.html' },
46
+ { name: 'gsub', full_name: 'String#gsub', type: 'instance_method', path: 'method/String/i/gsub.html' },
47
+ ])};\n"
48
+ PAGES = {
49
+ 'js/search_data.js' => INDEX_JS,
50
+ 'method/String/i/gsub.md' => "# String#gsub\n\n置き換えます。\n",
51
+ }.freeze
52
+
53
+ def build_markdown_db(dir)
54
+ api = File.join(dir, 'manual', 'api')
55
+ FileUtils.mkdir_p(File.join(api, 'foo'))
56
+ File.write(File.join(api, 'foo.md'), "---\ntype: library\n---\nfoo ライブラリ。\n")
57
+ File.write(File.join(api, 'foo', 'Foo.md'), FOO_MD)
58
+ prefix = File.join(dir, 'db')
59
+ db = BitClust::MethodDatabase.new(prefix)
60
+ db.init
61
+ db.transaction do
62
+ db.propset('version', '3.4')
63
+ db.propset('encoding', 'utf-8')
64
+ end
65
+ db.transaction do
66
+ db.update_by_markdowntree(api)
67
+ end
68
+ BitClust::MethodDatabase.new(prefix)
69
+ end
70
+
71
+ def with_db(&block)
72
+ Dir.mktmpdir do |dir|
73
+ yield build_markdown_db(dir)
74
+ end
75
+ end
76
+
77
+ # 既定の DB 探索(環境変数・設定ファイル)がどれも当たらない状態
78
+ DB_ENV_KEYS = %w[REFE2_SERVER BITCLUST_SERVER REFE2_DATADIR BITCLUST_DATADIR
79
+ HOME XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME].freeze
80
+
81
+ def without_local_db
82
+ saved = DB_ENV_KEYS.to_h {|k| [k, ENV[k]] }
83
+ Dir.mktmpdir do |home|
84
+ DB_ENV_KEYS.each {|k| ENV.delete(k) }
85
+ ENV['HOME'] = home
86
+ yield home
87
+ end
88
+ ensure
89
+ saved.each {|k, v| v ? ENV[k] = v : ENV.delete(k) }
90
+ end
91
+
92
+ def fake_remote(dir, pages = PAGES)
93
+ fetcher = lambda do |uri, _headers|
94
+ rel = uri.to_s.delete_prefix(BASE_URL)
95
+ pages.key?(rel) ? [200, pages[rel].dup] : [404, '']
96
+ end
97
+ BitClust::Remote.new(base_url: BASE_URL, fetcher: fetcher, cache_dir: File.join(dir, 'cache'))
98
+ end
99
+
100
+ def never_remote(dir)
101
+ BitClust::Remote.new(base_url: BASE_URL, cache_dir: File.join(dir, 'cache'),
102
+ fetcher: ->(_uri, _headers) { flunk 'remote must not be used' })
103
+ end
104
+
105
+ def test_terminal_view_writes_to_given_io
106
+ out = StringIO.new
107
+ view = BitClust::TerminalView.new(BitClust::Plain.new, {}, io: out)
108
+ view.send(:puts, 'hello')
109
+ assert_equal("hello\n", out.string)
110
+ assert_same(out, view.io)
111
+ assert_same($stdout, BitClust::TerminalView.new(BitClust::Plain.new, {}).io)
112
+ end
113
+
114
+ def test_lookup_method
115
+ with_db do |db|
116
+ out = StringIO.new
117
+ BitClust::Irb.lookup('Foo#bar', db: db, io: out)
118
+ assert_match(/Foo#bar/, out.string)
119
+ assert_match(/bar の説明。/, out.string)
120
+ end
121
+ end
122
+
123
+ def test_lookup_class
124
+ with_db do |db|
125
+ out = StringIO.new
126
+ BitClust::Irb.lookup('Foo', db: db, io: out)
127
+ assert_match(/class Foo < Object/, out.string)
128
+ assert_match(/クラスの説明。/, out.string)
129
+ end
130
+ end
131
+
132
+ def test_lookup_empty_pattern_shows_usage
133
+ with_db do |db|
134
+ out = StringIO.new
135
+ assert_nothing_raised do
136
+ BitClust::Irb.lookup('', db: db, io: out)
137
+ end
138
+ assert_match(/refe/, out.string)
139
+ end
140
+ end
141
+
142
+ def test_lookup_not_found_reports_instead_of_raising
143
+ with_db do |db|
144
+ out = StringIO.new
145
+ assert_nothing_raised do
146
+ BitClust::Irb.lookup('NoSuchClass#no_such_method', db: db, io: out)
147
+ end
148
+ assert_match(/no such method/, out.string)
149
+ end
150
+ end
151
+
152
+ def test_command_is_registered_for_irb
153
+ begin
154
+ require 'irb/command'
155
+ rescue LoadError
156
+ omit 'irb >= 1.13 is not available'
157
+ end
158
+ assert(BitClust::Irb::RefeCommand < ::IRB::Command::Base)
159
+ assert_match(/るりま|リファレンス/, BitClust::Irb::RefeCommand.description)
160
+ end
161
+
162
+ def test_searcher_local_database_p
163
+ without_local_db do
164
+ assert_false(BitClust::Searcher.new.local_database?)
165
+ with_db do |db|
166
+ ENV['BITCLUST_DATADIR'] = db.instance_variable_get(:@prefix)
167
+ assert_true(BitClust::Searcher.new.local_database?)
168
+ end
169
+ end
170
+ end
171
+
172
+ def test_lookup_uses_local_db_when_given
173
+ with_db do |db|
174
+ Dir.mktmpdir do |dir|
175
+ out = StringIO.new
176
+ BitClust::Irb.lookup('Foo#bar', db: db, io: out, remote: never_remote(dir))
177
+ assert_match(/bar の説明。/, out.string)
178
+ end
179
+ end
180
+ end
181
+
182
+ def test_lookup_falls_back_to_remote_without_local_db
183
+ without_local_db do |home|
184
+ out = StringIO.new
185
+ BitClust::Irb.lookup('String#gsub', io: out, remote: fake_remote(home))
186
+ assert_match(/置き換えます。/, out.string)
187
+ assert_match(%r{method/String/i/gsub\.html}, out.string)
188
+ end
189
+ end
190
+
191
+ def test_lookup_without_remote_and_local_db_shows_setup_hint
192
+ without_local_db do
193
+ out = StringIO.new
194
+ assert_nothing_raised do
195
+ BitClust::Irb.lookup('String#gsub', io: out, remote: nil)
196
+ end
197
+ assert_match(/bitclust setup/, out.string)
198
+ end
199
+ end
200
+
201
+ def test_refe_cli_falls_back_to_remote_without_local_db
202
+ without_local_db do |home|
203
+ out = StringIO.new
204
+ searcher = BitClust::Searcher.new
205
+ searcher.remote = fake_remote(home)
206
+ searcher.parse(%w[String#gsub])
207
+ view = BitClust::TerminalView.new(BitClust::Plain.new, {}, io: out)
208
+ searcher.run_query(nil, %w[String#gsub], view)
209
+ assert_match(/置き換えます。/, out.string)
210
+ end
211
+ end
212
+ end
@@ -38,7 +38,7 @@ class TestLinkChecker < Test::Unit::TestCase
38
38
 
39
39
  ## Class Methods
40
40
 
41
- ### def baz -> nil
41
+ ### def Foo.baz -> nil
42
42
 
43
43
  baz です。
44
44
 
@@ -91,6 +91,41 @@ class TestMarkdownBridge < Test::Unit::TestCase
91
91
  out["thread.rd"]
92
92
  end
93
93
 
94
+ def test_membership_version_and_else_wrap_include_sites
95
+ # bitclust#331: #@version V は #@if (version == "V")、その #@else は
96
+ # #@if (version != "V") で再具現化する。範囲の #@else は 2 つの
97
+ # membership(A 未満 / B 以上)になるので include サイトも 2 つ
98
+ out = build_from(
99
+ "builtin.md" => "---\ntype: library\n---\nbuiltin 概要。\n",
100
+ "onlylib.md" => "---\ntype: library\n---\nonlylib 概要。\n",
101
+ "range.md" => "---\ntype: library\n---\nrange 概要。\n",
102
+ "onlylib/Shared.md" =>
103
+ "---\n" \
104
+ "library:\n" \
105
+ "\#@version 3.2\n" \
106
+ " - onlylib\n" \
107
+ "\#@else\n" \
108
+ " - builtin\n" \
109
+ "\#@end\n" \
110
+ "\#@version 3.1...3.3\n" \
111
+ " - range\n" \
112
+ "\#@else\n" \
113
+ " - range\n" \
114
+ "\#@end\n" \
115
+ "---\n" \
116
+ "# class Shared < Object\nS。\n"
117
+ )
118
+ assert_equal "onlylib 概要。\n\n\#@if (version == \"3.2\")\n\#@include(onlylib/Shared)\n\#@end\n",
119
+ out["onlylib.rd"]
120
+ assert_equal "builtin 概要。\n\n\#@if (version != \"3.2\")\n\#@include(onlylib/Shared)\n\#@end\n",
121
+ out["builtin.rd"]
122
+ assert_equal "range 概要。\n\n" \
123
+ "\#@until 3.3\n\#@since 3.1\n\#@include(onlylib/Shared)\n\#@end\n\#@end\n" \
124
+ "\#@until 3.1\n\#@include(onlylib/Shared)\n\#@end\n" \
125
+ "\#@since 3.3\n\#@include(onlylib/Shared)\n\#@end\n",
126
+ out["range.rd"]
127
+ end
128
+
94
129
  def test_libraries_manifest
95
130
  out = build
96
131
  assert_equal "bar/baz\ndual\nfoo\n\#@until 3.1\ngated\n\#@end\nsub/sub\n", out["LIBRARIES"]
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test/unit'
4
+ require 'stringio'
5
+ require 'bitclust'
6
+ require 'bitclust/markdown_exporter'
7
+ require 'bitclust/mdparser'
8
+ require 'bitclust/methoddatabase'
9
+ require 'bitclust/functiondatabase'
10
+ require 'bitclust/subcommands/statichtml_command'
11
+
12
+ # MarkdownExporter: statichtml --markdown-output 用に、HTML の各ページと
13
+ # 対になる Markdown を前処理済みソースから組み立てる。
14
+ class TestMarkdownExporter < Test::Unit::TestCase
15
+ PARAMS = { "version" => "3.4" }
16
+
17
+ def setup
18
+ @urlmapper = BitClust::Subcommands::StatichtmlCommand::URLMapperEx.new(
19
+ :suffix => '.html',
20
+ :fs_casesensitive => true
21
+ )
22
+ @urlmapper.bitclust_html_base = '..'
23
+ @exporter = BitClust::MarkdownExporter.new(@urlmapper, '.html')
24
+ end
25
+
26
+ def parse_md(md, libname = "_builtin")
27
+ db = BitClust::MethodDatabase.dummy(PARAMS)
28
+ lib = BitClust::MDParser.new(db).parse(StringIO.new(md), libname, PARAMS)
29
+ [db, lib]
30
+ end
31
+
32
+ sub_test_case("convert_text") do
33
+ def test_bare_method_ref
34
+ assert_equal "[Array#index](../method/Array/i/index.md) を参照",
35
+ @exporter.convert_text("[m:Array#index] を参照")
36
+ end
37
+
38
+ def test_bare_class_ref
39
+ assert_equal "[String](../class/String.md)",
40
+ @exporter.convert_text("[c:String]")
41
+ end
42
+
43
+ def test_bare_library_ref
44
+ assert_equal "[csv](../library/csv.md)",
45
+ @exporter.convert_text("[lib:csv]")
46
+ end
47
+
48
+ def test_bare_doc_ref
49
+ assert_equal "[spec/literal](../doc/spec=2fliteral.md)",
50
+ @exporter.convert_text("[d:spec/literal]")
51
+ end
52
+
53
+ def test_bare_function_ref
54
+ assert_equal "[rb_ary_new](../function/rb_ary_new.md)",
55
+ @exporter.convert_text("[f:rb_ary_new]")
56
+ end
57
+
58
+ def test_module_function_ref
59
+ # md 表記の ?. は内部表記 .# に正規化して URL を引く(ラベルは書かれたまま)
60
+ assert_equal "[Kernel?.puts](../method/Kernel/m/puts.md)",
61
+ @exporter.convert_text("[m:Kernel?.puts]")
62
+ end
63
+
64
+ def test_method_ref_with_escaped_brackets
65
+ # [m:Array#\[\]] のようにエスケープされた参照。URL はエスケープを解いた
66
+ # メソッド名から引き、ラベルは書かれたまま(md として正しい形)を保つ
67
+ assert_equal "[Array#\\[\\]](../method/Array/i/=5b=5d.md)",
68
+ @exporter.convert_text("[m:Array#\\[\\]]")
69
+ end
70
+
71
+ def test_labeled_ref
72
+ assert_equal "[個数](../method/Array/i/size.md)",
73
+ @exporter.convert_text("[個数](m:Array#size)")
74
+ end
75
+
76
+ def test_labeled_ref_with_code_span_label
77
+ assert_equal "[`Array#size`](../method/Array/i/size.md)",
78
+ @exporter.convert_text("[`Array#size`](m:Array#size)")
79
+ end
80
+
81
+ def test_markdown_link_kept
82
+ assert_equal "[Ruby](https://www.ruby-lang.org/)",
83
+ @exporter.convert_text("[Ruby](https://www.ruby-lang.org/)")
84
+ end
85
+
86
+ def test_anchor_link_kept
87
+ assert_equal "[前述の例](#example)",
88
+ @exporter.convert_text("[前述の例](#example)")
89
+ end
90
+
91
+ def test_url_ref
92
+ assert_equal "[https://www.ruby-lang.org/](https://www.ruby-lang.org/)",
93
+ @exporter.convert_text("[url:https://www.ruby-lang.org/]")
94
+ end
95
+
96
+ def test_ref_in_page
97
+ assert_equal "[overview](#overview)",
98
+ @exporter.convert_text("[ref:overview]")
99
+ end
100
+
101
+ def test_ref_cross_page
102
+ assert_equal "[spec/literal#num](../doc/spec=2fliteral.md#num)",
103
+ @exporter.convert_text("[ref:d:spec/literal#num]")
104
+ end
105
+
106
+ def test_code_span_is_not_converted
107
+ assert_equal "`[m:Array#index]`",
108
+ @exporter.convert_text("`[m:Array#index]`")
109
+ end
110
+
111
+ def test_code_fence_is_not_converted
112
+ src = "```ruby\n# [m:Array#index]\n```\n[m:Array#index]\n"
113
+ expected = "```ruby\n# [m:Array#index]\n```\n[Array#index](../method/Array/i/index.md)\n"
114
+ assert_equal expected, @exporter.convert_text(src)
115
+ end
116
+
117
+ def test_unknown_ref_type_is_kept
118
+ assert_equal "[man:printf(3)]", @exporter.convert_text("[man:printf(3)]")
119
+ end
120
+
121
+ def test_unresolvable_method_spec_is_kept
122
+ # クラス名とメソッド名に分割できない指定はそのまま残す
123
+ assert_equal "[m:broken spec]", @exporter.convert_text("[m:broken spec]")
124
+ end
125
+
126
+ def test_escaped_bracket_is_kept
127
+ assert_equal "\\[m:Array#index]", @exporter.convert_text("\\[m:Array#index]")
128
+ end
129
+ end
130
+
131
+ sub_test_case("pages") do
132
+ CLASS_MD = <<~MD
133
+ ---
134
+ library: _builtin
135
+ ---
136
+ # class Array < Object
137
+
138
+ 配列クラス。[c:Enumerable] も参照。
139
+
140
+ ## Class Methods
141
+
142
+ ### def self.new(size) -> Array
143
+
144
+ 生成。
145
+
146
+ ## Instance Methods
147
+
148
+ ### def index(val) -> Integer
149
+ ### def find_index(val) -> Integer
150
+
151
+ 位置を返します。
152
+ MD
153
+
154
+ def test_class_page
155
+ _, lib = parse_md(CLASS_MD)
156
+ md = @exporter.class_page(lib.classes.first)
157
+ assert_equal <<~MD, md
158
+ # class Array < Object
159
+
160
+ 配列クラス。[Enumerable](../class/Enumerable.md) も参照。
161
+
162
+ ## Class Methods
163
+
164
+ - [new](../method/Array/s/new.md)
165
+
166
+ ## Instance Methods
167
+
168
+ - [find_index](../method/Array/i/find_index.md)
169
+ - [index](../method/Array/i/index.md)
170
+ MD
171
+ end
172
+
173
+ def test_module_page_heading
174
+ md = <<~MD
175
+ ---
176
+ library: _builtin
177
+ ---
178
+ # module Comparable
179
+
180
+ 比較。
181
+ MD
182
+ _, lib = parse_md(md)
183
+ page = @exporter.class_page(lib.classes.first)
184
+ assert page.start_with?("# module Comparable\n"), page
185
+ end
186
+
187
+ def test_method_page
188
+ _, lib = parse_md(CLASS_MD)
189
+ entry = lib.classes.first.entries.find { |e| e.names.include?("index") }
190
+ @urlmapper.bitclust_html_base = '../../..'
191
+ md = @exporter.method_page("Array#index", [entry])
192
+ assert_equal <<~MD, md
193
+ # Array#index
194
+
195
+ ### def index(val) -> Integer
196
+ ### def find_index(val) -> Integer
197
+
198
+ 位置を返します。
199
+ MD
200
+ end
201
+
202
+ def test_method_page_normalizes_module_function_name
203
+ _, lib = parse_md(CLASS_MD)
204
+ entry = lib.classes.first.entries.first
205
+ md = @exporter.method_page("Kernel.#puts", [entry])
206
+ assert md.start_with?("# Kernel?.puts\n"), md
207
+ end
208
+
209
+ def test_library_page
210
+ md = <<~MD
211
+ ---
212
+ type: library
213
+ category: FileFormat
214
+ ---
215
+ CSV を扱うライブラリです。
216
+ MD
217
+ _, lib = parse_md(md, "csv")
218
+ page = @exporter.library_page(lib)
219
+ assert_equal <<~MD, page
220
+ # library csv
221
+
222
+ CSV を扱うライブラリです。
223
+ MD
224
+ end
225
+
226
+ def test_doc_page
227
+ db = BitClust::MethodDatabase.dummy(PARAMS)
228
+ entry = BitClust::DocEntry.new(db, "help")
229
+ entry.title = "ヘルプ"
230
+ entry.source = "\n[c:String] を参照。\n"
231
+ page = @exporter.doc_page(entry)
232
+ assert_equal <<~MD, page
233
+ # ヘルプ
234
+
235
+ [String](../class/String.md) を参照。
236
+ MD
237
+ end
238
+
239
+ def test_function_page
240
+ require 'bitclust/functiondatabase'
241
+ md = <<~MD
242
+ ### VALUE rb_ary_new()
243
+
244
+ 空の配列を返します。
245
+ MD
246
+ db = BitClust::FunctionDatabase.dummy(PARAMS)
247
+ io = StringIO.new(md)
248
+ def io.path = "array.c.md"
249
+ BitClust::MDFunctionParser.new(db).parse(io, "array.c", PARAMS)
250
+ page = @exporter.function_page(db.functions.first)
251
+ assert_equal <<~MD, page
252
+ # rb_ary_new
253
+
254
+ ### VALUE rb_ary_new()
255
+
256
+ 空の配列を返します。
257
+ MD
258
+ end
259
+ end
260
+ end
@@ -366,4 +366,148 @@ class TestMarkdownTree < Test::Unit::TestCase
366
366
  "version-range-gated H1 relations must not be flagged"
367
367
  assert_equal [], tree.warnings
368
368
  end
369
+
370
+ # --- front matter ゲート付きリストのディレクティブ(bitclust#331) ---
371
+ # テストリスト:
372
+ # [x] #%else は直前ゲートの反転(until → since / since → until)
373
+ # [x] #%version A...B / A... / ...B / V(単一版)
374
+ # [x] #%version A...B の #%else は「A 未満」と「B 以上」の 2 membership
375
+ # [x] #%version V の #%else は except: [V]
376
+ # [x] 入れ子のゲートは交差(since は max・until は min)
377
+ # [x] 矛盾する入れ子(単一版 ≠ 単一版)は membership を作らない
378
+ # [x] #%# コメント行は無視
379
+ # [x] 未知の #% 行・#%if・対応のない #%else/#%end・二重 #%else・
380
+ # リスト終端で閉じていないゲートは ParseError(無音の打ち切りをやめる)
381
+
382
+ def membership_scan(list)
383
+ scan(
384
+ "a.md" => LIB, "b.md" => LIB, "c.md" => LIB,
385
+ "a/X.md" => "---\nlibrary:\n#{list}---\n# class X < Object\n"
386
+ )
387
+ end
388
+
389
+ def memberships_of(list)
390
+ tree = membership_scan(list)
391
+ assert_equal [], tree.warnings
392
+ tree.entities["a/X.md"][:memberships]
393
+ end
394
+
395
+ def test_else_inverts_until
396
+ assert_equal [
397
+ { library: "a", until: "3.4" },
398
+ { library: "b", since: "3.4" },
399
+ ], memberships_of("#%until 3.4\n - a\n#%else\n - b\n#%end\n")
400
+ end
401
+
402
+ def test_else_inverts_since
403
+ assert_equal [
404
+ { library: "a", since: "3.4" },
405
+ { library: "b", until: "3.4" },
406
+ ], memberships_of("#%since 3.4\n - a\n#%else\n - b\n#%end\n")
407
+ end
408
+
409
+ def test_version_range_forms
410
+ assert_equal [
411
+ { library: "a", since: "3.1", until: "3.3" },
412
+ { library: "b", since: "3.1" },
413
+ { library: "c", until: "3.3" },
414
+ ], memberships_of(
415
+ "#%version 3.1...3.3\n - a\n#%end\n" \
416
+ "#%version 3.1...\n - b\n#%end\n" \
417
+ "#%version ...3.3\n - c\n#%end\n"
418
+ )
419
+ end
420
+
421
+ def test_single_version
422
+ assert_equal [{ library: "a", version: "3.1" }],
423
+ memberships_of("#%version 3.1\n - a\n#%end\n")
424
+ end
425
+
426
+ def test_else_of_version_range_splits_into_two_memberships
427
+ assert_equal [
428
+ { library: "a", since: "3.1", until: "3.3" },
429
+ { library: "b", until: "3.1" },
430
+ { library: "b", since: "3.3" },
431
+ ], memberships_of("#%version 3.1...3.3\n - a\n#%else\n - b\n#%end\n")
432
+ end
433
+
434
+ def test_else_of_single_version_is_except
435
+ assert_equal [
436
+ { library: "a", version: "3.1" },
437
+ { library: "b", except: ["3.1"] },
438
+ ], memberships_of("#%version 3.1\n - a\n#%else\n - b\n#%end\n")
439
+ end
440
+
441
+ def test_nested_gates_intersect
442
+ assert_equal [
443
+ { library: "a", since: "3.2", until: "3.4" },
444
+ { library: "b", since: "3.0", until: "3.2" },
445
+ ], memberships_of(
446
+ "#%since 3.0\n#%until 3.4\n#%since 3.2\n - a\n#%else\n - b\n#%end\n#%end\n#%end\n"
447
+ )
448
+ end
449
+
450
+ def test_contradicting_nested_single_versions_yield_no_membership
451
+ assert_equal [{ library: "b", version: "3.1" }],
452
+ memberships_of("#%version 3.1\n#%version 3.2\n - a\n#%end\n - b\n#%end\n")
453
+ end
454
+
455
+ def test_directive_comment_lines_are_ignored
456
+ assert_equal [{ library: "a" }, { library: "b", until: "3.4" }],
457
+ memberships_of("#%# comment\n - a\n#%until 3.4\n#%# another\n - b\n#%end\n")
458
+ end
459
+
460
+ def test_at_prefix_else_and_version
461
+ assert_equal [
462
+ { library: "a", since: "3.1", until: "3.3" },
463
+ { library: "b", until: "3.1" },
464
+ { library: "b", since: "3.3" },
465
+ ], memberships_of("\#@version 3.1...3.3\n - a\n\#@else\n - b\n\#@end\n")
466
+ end
467
+
468
+ def assert_list_parse_error(list, pattern)
469
+ e = assert_raise(BitClust::ParseError) { membership_scan(list) }
470
+ assert_match pattern, e.message
471
+ assert_match(/a\/X\.md:\d+/, e.message, "message must locate the file and line")
472
+ end
473
+
474
+ def test_unknown_directive_in_list_raises
475
+ assert_list_parse_error(" - a\n#%sinse 3.4\n - b\n#%end\n", /unknown preprocessor directive/)
476
+ end
477
+
478
+ def test_if_in_list_is_unsupported
479
+ assert_list_parse_error("#%if (version >= \"3.4\")\n - a\n#%end\n", /#%if is not supported/)
480
+ end
481
+
482
+ def test_else_without_gate_raises
483
+ assert_list_parse_error(" - a\n#%else\n - b\n#%end\n", /no matching/)
484
+ end
485
+
486
+ def test_end_without_gate_raises
487
+ assert_list_parse_error(" - a\n#%end\n", /no matching/)
488
+ end
489
+
490
+ def test_duplicate_else_raises
491
+ assert_list_parse_error("#%since 3.4\n - a\n#%else\n - b\n#%else\n - c\n#%end\n", /duplicate/)
492
+ end
493
+
494
+ def test_unterminated_gate_at_list_end_raises
495
+ assert_list_parse_error("#%since 3.4\n - a\n", /unterminated/)
496
+ end
497
+
498
+ def test_indented_directive_in_list_raises
499
+ # 列 0 でない #% 行は Preprocessor でも指令にならない(YAML コメント扱い)。
500
+ # リスト内で無音にゲートが無視されたりリストが打ち切られたりしないよう、
501
+ # 指令風のインデント行はエラーにする
502
+ assert_list_parse_error(" - a\n #%since 3.0\n - b\n#%end\n", /must start at column 0/)
503
+ end
504
+
505
+ def test_yaml_comment_in_list_is_ignored
506
+ assert_equal [{ library: "a" }, { library: "b", since: "3.0" }],
507
+ memberships_of(" # plain yaml comment\n - a\n#%since 3.0\n # another\n - b\n#%end\n")
508
+ end
509
+
510
+ def test_wrong_version_range_raises
511
+ assert_list_parse_error("#%version 3.1..3.3\n - a\n#%end\n", /wrong version range/)
512
+ end
369
513
  end