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,478 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test/unit'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+
7
+ require 'bitclust/markdown_orchestrator'
8
+ require 'bitclust/markdown_to_rrd'
9
+
10
+ # MarkdownOrchestrator: RD ツリー → Markdown ツリー変換のクロスファイル方針を
11
+ # 1か所に束ねる(グラフ解析 → prune → 全体ゲート解除 → front matter 注入)。
12
+ # bin/rrd2md --graph と tools/md-roundtrip-check.rb --inject の共通実装。
13
+ #
14
+ # テストリスト:
15
+ # [x] member には library を注入し、root からは grouping include を prune、
16
+ # root には type: library を付ける
17
+ # [x] fragment include は温存される
18
+ # [x] 全体ゲート(常に真)は解除されて md にラッパーが残らない
19
+ # [x] LIBRARIES 由来のゲートとファイル全体ゲートは交差でマージされ二重にならない
20
+ # [x] reduce の rd と convert の md が MarkdownToRRD で一致(ラウンドトリップ期待値)
21
+ # [x] LIBRARIES 自体は変換対象外
22
+ class TestMarkdownOrchestrator < Test::Unit::TestCase
23
+ FILES = {
24
+ "LIBRARIES" => "foo\n\#@until 3.1\ngated\n\#@end\nx\nx/x\ncml\ngml\n",
25
+ "foo.rd" => "category Cat\n\n説明。\n\n\#@include(foo/Bar)\n\n\#@include(foo/frag)\n",
26
+ "foo/Bar" => "\#@since 1.9.1\n= class Bar < Object\n\nBar の説明。\n\#@end\n",
27
+ "foo/frag" => "断片。\n",
28
+ "gated.rd" => "\#@until 3.1\ngated ライブラリの説明。\n\#@end\n",
29
+ "x.rd" => "x の説明。\n\n\#@include(x/X)\n",
30
+ "x/X" => "= class X < Object\n\nX の説明。\n",
31
+ "x/x.rd" => "x/x の説明。\n",
32
+ # cmath 型: LIBRARIES にゲートは無いがファイル全体が #@since で包まれている
33
+ "cml.rd" => "\#@since 1.9.1\ncml の説明。\n\#@end\n",
34
+ # rubygems 型: 据え置きゲートの中にライブラリメタデータがある
35
+ "gml.rd" => "\#@since 1.9.1\nrequire foo\n\ngml の説明。\n\#@end\n",
36
+ }.freeze
37
+
38
+ def with_orchestrator(scope: nil)
39
+ Dir.mktmpdir do |dir|
40
+ FILES.each do |path, content|
41
+ full = File.join(dir, path)
42
+ FileUtils.mkdir_p(File.dirname(full))
43
+ File.write(full, content)
44
+ end
45
+ opts = scope ? { scope: scope } : {}
46
+ yield BitClust::MarkdownOrchestrator.new(dir, **opts)
47
+ end
48
+ end
49
+
50
+ def wide_scope
51
+ BitClust::IncludeGraph::Scope.new("1.8.7", "4.2")
52
+ end
53
+
54
+ def test_member_whole_file_gate_unwraps_to_entity_since_under_wide_scope
55
+ # メンバーの全体ゲートはエンティティ自身の存在ゲートなので front matter へ
56
+ # (旧世界でも include 先が空にプリプロセスされる=エンティティ不存在で等価)
57
+ with_orchestrator(scope: wide_scope) do |orch|
58
+ md = orch.convert("foo/Bar", FILES["foo/Bar"])
59
+ expected = <<~'MD'
60
+ ---
61
+ library: foo
62
+ since: "1.9.1"
63
+ ---
64
+ # class Bar < Object
65
+
66
+ Bar の説明。
67
+ MD
68
+ assert_equal expected, md
69
+ end
70
+ end
71
+
72
+ def test_fragment_whole_file_gate_is_kept_in_body
73
+ # 断片(#@include 展開用、front matter を持てない)の全体ゲートは
74
+ # 常真でない限り据え置く。front matter を付けると include 展開の途中に
75
+ # `---` が現れてパースが壊れる(_builtin/Fiber.current の #@since 1.9.0)
76
+ files = {
77
+ "LIBRARIES" => "foo\n",
78
+ "foo.rd" => "説明。\n\n\#@include(foo/gfrag)\n",
79
+ "foo/gfrag" => "\#@since 1.9.1\n断片。\n\#@end\n",
80
+ }
81
+ with_files(files, scope: wide_scope) do |orch|
82
+ md = orch.convert("foo/gfrag", files["foo/gfrag"])
83
+ assert_equal "\#@since 1.9.1\n断片。\n\#@end\n", md
84
+ end
85
+ # デフォルトスコープでは常真 → 従来どおり解除される
86
+ with_files(files) do |orch|
87
+ md = orch.convert("foo/gfrag", files["foo/gfrag"])
88
+ assert_equal "断片。\n", md
89
+ end
90
+ end
91
+
92
+ def with_files(files, scope: nil)
93
+ Dir.mktmpdir do |dir|
94
+ files.each do |path, content|
95
+ full = File.join(dir, path)
96
+ FileUtils.mkdir_p(File.dirname(full))
97
+ File.write(full, content)
98
+ end
99
+ opts = scope ? { scope: scope } : {}
100
+ yield BitClust::MarkdownOrchestrator.new(dir, **opts)
101
+ end
102
+ end
103
+
104
+ def test_kept_gate_library_metadata_moves_to_front_matter_with_gate
105
+ # rubygems 型: 据え置きゲートの中の require 等は、独立ゲートに分離した上で
106
+ # front matter 化する(native パースはメタデータを front matter からしか
107
+ # 読まないため)。本文はゲートのまま残る
108
+ with_orchestrator(scope: wide_scope) do |orch|
109
+ md = orch.convert("gml.rd", FILES["gml.rd"])
110
+ expected = <<~'MD'
111
+ ---
112
+ type: library
113
+ require:
114
+ #@since 1.9.1
115
+ - foo
116
+ #@end
117
+ ---
118
+ #@since 1.9.1
119
+ gml の説明。
120
+ #@end
121
+ MD
122
+ assert_equal expected, md
123
+ end
124
+ end
125
+
126
+ def test_narrowing_whole_file_gate_on_library_root_is_kept_in_body
127
+ # cmath 型: ライブラリの存在は LIBRARIES が正(1.8.7 にも存在して内容が空)。
128
+ # LIBRARIES 由来ゲートより狭いファイル全体ゲートを front matter に解除すると
129
+ # 「ライブラリの存在」まで狭めてしまうので、据え置いて本文ゲートのまま残す
130
+ with_orchestrator(scope: wide_scope) do |orch|
131
+ md = orch.convert("cml.rd", FILES["cml.rd"])
132
+ expected = <<~'MD'
133
+ ---
134
+ type: library
135
+ ---
136
+ #@since 1.9.1
137
+ cml の説明。
138
+ #@end
139
+ MD
140
+ assert_equal expected, md
141
+ end
142
+ end
143
+
144
+ def test_matching_whole_file_gate_still_unwraps_under_wide_scope
145
+ # LIBRARIES 由来のゲート(until 3.1)とファイル全体ゲートが一致する場合は
146
+ # 従来どおり解除して front matter に一本化する(fiber/set 型)
147
+ with_orchestrator(scope: wide_scope) do |orch|
148
+ md = orch.convert("gated.rd", FILES["gated.rd"])
149
+ expected = <<~'MD'
150
+ ---
151
+ type: library
152
+ until: "3.1"
153
+ ---
154
+ gated ライブラリの説明。
155
+ MD
156
+ assert_equal expected, md
157
+ end
158
+ end
159
+
160
+ def test_root_conversion_prunes_groupings_and_marks_library
161
+ with_orchestrator do |orch|
162
+ md = orch.convert("foo.rd", FILES["foo.rd"])
163
+ expected = <<~MD
164
+ ---
165
+ type: library
166
+ category: Cat
167
+ ---
168
+ 説明。
169
+
170
+ \#@include(foo/frag)
171
+ MD
172
+ assert_equal expected, md
173
+ end
174
+ end
175
+
176
+ def test_member_conversion_injects_library_and_unwraps_vacuous_gate
177
+ with_orchestrator do |orch|
178
+ md = orch.convert("foo/Bar", FILES["foo/Bar"])
179
+ expected = <<~MD
180
+ ---
181
+ library: foo
182
+ ---
183
+ # class Bar < Object
184
+
185
+ Bar の説明。
186
+ MD
187
+ assert_equal expected, md
188
+ end
189
+ end
190
+
191
+ def test_library_gate_from_libraries_and_file_wrap_merge_once
192
+ with_orchestrator do |orch|
193
+ md = orch.convert("gated.rd", FILES["gated.rd"])
194
+ expected = <<~MD
195
+ ---
196
+ type: library
197
+ until: "3.1"
198
+ ---
199
+ gated ライブラリの説明。
200
+ MD
201
+ assert_equal expected, md
202
+ end
203
+ end
204
+
205
+ def test_reduce_matches_markdown_to_rrd_of_convert
206
+ with_orchestrator do |orch|
207
+ FILES.each_key do |path|
208
+ next unless orch.convert?(path)
209
+ rrd = FILES[path]
210
+ reduced, = orch.reduce(path, rrd)
211
+ assert_equal reduced, BitClust::MarkdownToRRD.convert(orch.convert(path, rrd)),
212
+ "roundtrip mismatch for #{path}"
213
+ end
214
+ end
215
+ end
216
+
217
+ def test_libraries_file_is_not_converted
218
+ with_orchestrator do |orch|
219
+ assert_false orch.convert?("LIBRARIES")
220
+ assert_true orch.convert?("foo.rd")
221
+ end
222
+ end
223
+
224
+ def test_entity_h1_without_space_is_normalized
225
+ # _builtin/Encoding の「=class Encoding」。RRDParser は受理するが
226
+ # 単一ファイル変換器は正規形「= class」のみ扱うため、reduce 段で正規化する
227
+ with_orchestrator do |orch|
228
+ rrd = "=class Enc\n\n説明。\n"
229
+ reduced, = orch.reduce("foo/Enc", rrd)
230
+ assert_equal "= class Enc\n\n説明。\n", reduced
231
+ assert_equal "# class Enc\n\n説明。\n", orch.convert("foo/Enc", rrd)
232
+ end
233
+ end
234
+
235
+ # ---- reduce のヘッダ正規化 ----
236
+ # md→rd の再生成形(H1 直後に関係、末尾空白なし)に reduce 側を揃え、
237
+ # byte-exact ラウンドトリップを成立させる
238
+
239
+ def test_reduce_strips_leading_blank_after_resolution
240
+ # base64/Base64: 解決で先頭に空行が残るケース
241
+ with_orchestrator do |orch|
242
+ rrd = "\#@since 1.9.1\n\n= module B\n\#@else\n= module Old\n\#@end\n"
243
+ reduced, = orch.reduce("foo/Bar", rrd)
244
+ assert_equal "= module B\n", reduced
245
+ end
246
+ end
247
+
248
+ def test_reduce_strips_trailing_space_on_relation_lines
249
+ # openssl/SSL__SSLSocket: 「include X 」の末尾スペース
250
+ with_orchestrator do |orch|
251
+ rrd = "= class A < Object\ninclude Foo \n\n説明。\n"
252
+ reduced, = orch.reduce("foo/Bar", rrd)
253
+ assert_equal "= class A < Object\ninclude Foo\n\n説明。\n", reduced
254
+ end
255
+ end
256
+
257
+ def test_reduce_removes_blank_between_h1_and_relations
258
+ # _builtin/Integer 等: H1 と(gated)関係の間の空行
259
+ with_orchestrator do |orch|
260
+ rrd = "= class A < Object\n\ninclude Foo\n\n説明。\n"
261
+ reduced, = orch.reduce("foo/Bar", rrd)
262
+ assert_equal "= class A < Object\ninclude Foo\n\n説明。\n", reduced
263
+
264
+ rrd2 = "= class A < B\n\n\#@until 3.2\nalias F\n\#@end\n\n説明。\n"
265
+ reduced2, = orch.reduce("foo/Bar", rrd2)
266
+ assert_equal "= class A < B\n\#@until 3.2\nalias F\n\#@end\n\n説明。\n", reduced2
267
+ end
268
+ end
269
+
270
+ def test_reduce_keeps_blank_after_h1_without_relations
271
+ with_orchestrator do |orch|
272
+ rrd = "= class A < Object\n\n説明。\n\#@since 3.0\n新しい。\n\#@end\n"
273
+ reduced, = orch.reduce("foo/Bar", rrd)
274
+ assert_equal rrd, reduced
275
+ end
276
+ end
277
+
278
+ # ---- units: エンティティ分割(O3)----
279
+ # ヘッダ関係を持つマルチエンティティファイルはエンティティ単位に分割し、
280
+ # 関係を front matter に一元化する。関係なしの束ね(Errno 族等)は分割しない。
281
+
282
+ def test_units_split_relation_bearing_multi_entity_file
283
+ with_orchestrator do |orch|
284
+ rrd = "= class Conf < Object\ninclude Enumerable\n\n説明。\n\n" \
285
+ "= class Conf::Error < StandardError\nエラー。\n"
286
+ units = orch.units("foo/Bar", rrd)
287
+ assert_equal ["foo/Conf.md", "foo/Conf__Error.md"], units.map(&:path)
288
+ assert_equal <<~MD, orch.convert_unit(units[0])
289
+ ---
290
+ library: foo
291
+ include:
292
+ - Enumerable
293
+ ---
294
+ # class Conf < Object
295
+
296
+ 説明。
297
+
298
+ MD
299
+ assert_equal <<~MD, orch.convert_unit(units[1])
300
+ ---
301
+ library: foo
302
+ ---
303
+ # class Conf::Error < StandardError
304
+ エラー。
305
+ MD
306
+ end
307
+ end
308
+
309
+ def test_units_keep_relation_free_bundle_unsplit
310
+ with_orchestrator do |orch|
311
+ rrd = "= class E1 < StandardError\n説明1。\n\n= class E2 < StandardError\n説明2。\n"
312
+ units = orch.units("foo/Bar", rrd)
313
+ assert_equal ["foo/Bar.md"], units.map(&:path)
314
+ end
315
+ end
316
+
317
+ def test_units_keep_out_of_scope_file_unsplit
318
+ # スコープ外ファイル(front matter 注入なし)は関係があっても分割しない。
319
+ # サルベージは別スコープの再実行で扱う(分割すると library なしの
320
+ # エンティティファイルが散乱する)
321
+ with_orchestrator do |orch|
322
+ rrd = "= class A < Object\ninclude Enumerable\n\nA。\n\n= class B < Object\nB。\n"
323
+ units = orch.units("unreachable/File", rrd)
324
+ assert_equal ["unreachable/File.md"], units.map(&:path)
325
+ end
326
+ end
327
+
328
+ def test_units_resolve_renamed_h1_into_single_entity
329
+ # thread/Mutex パターン: 常真ゲートの改名 H1 ペア → 分割ではなく単一エンティティ化。
330
+ # alias が front matter に上がる
331
+ with_orchestrator do |orch|
332
+ rrd = "\#@since 2.3.0\n= class T::Mutex < Object\nalias Mutex\n\#@else\n" \
333
+ "= class Mutex < Object\n\#@end\n\n本文。\n"
334
+ units = orch.units("foo/Bar", rrd)
335
+ assert_equal ["foo/Bar.md"], units.map(&:path)
336
+ assert_equal <<~MD, orch.convert_unit(units[0])
337
+ ---
338
+ library: foo
339
+ alias:
340
+ - Mutex
341
+ ---
342
+ # class T::Mutex < Object
343
+
344
+ 本文。
345
+ MD
346
+ end
347
+ end
348
+
349
+ def test_units_move_segment_gate_to_front_matter
350
+ # スコープ内ゲート付きエンティティを含む分割: セグメントの全体ゲートが since に
351
+ with_orchestrator do |orch|
352
+ rrd = "= class Conf < Object\ninclude Enumerable\n\n説明。\n\n" \
353
+ "\#@since 3.2\n= class Conf::New < Object\n新しい。\n\#@end\n"
354
+ units = orch.units("foo/Bar", rrd)
355
+ assert_equal ["foo/Conf.md", "foo/Conf__New.md"], units.map(&:path)
356
+ assert_equal <<~MD, orch.convert_unit(units[1])
357
+ ---
358
+ library: foo
359
+ since: "3.2"
360
+ ---
361
+ # class Conf::New < Object
362
+ 新しい。
363
+ MD
364
+ end
365
+ end
366
+
367
+ def test_units_single_entity_file_passes_through
368
+ with_orchestrator do |orch|
369
+ units = orch.units("foo/Bar", FILES["foo/Bar"])
370
+ assert_equal ["foo/Bar.md"], units.map(&:path)
371
+ assert_equal orch.convert("foo/Bar", FILES["foo/Bar"]), orch.convert_unit(units[0])
372
+ end
373
+ end
374
+
375
+ def test_units_library_file_keeps_own_path
376
+ with_orchestrator do |orch|
377
+ units = orch.units("foo.rd", FILES["foo.rd"])
378
+ assert_equal ["foo.md"], units.map(&:path)
379
+ end
380
+ end
381
+
382
+ # ---- ライブラリファイルのインライン・エンティティ分割 ----
383
+
384
+ def test_units_split_inline_entities_out_of_library_file
385
+ # sdbm.rd 型: ライブラリ概要 + インライン・エンティティ複数(関係あり)。
386
+ # エンティティは <libname>/ 配下へ、library を注入。概要部は元パスに残る
387
+ with_orchestrator do |orch|
388
+ rrd = "category Cat\n\n概要。\n\n= class A < Object\ninclude Enumerable\n\nA。\n\n= class A::E < StandardError\nE。\n"
389
+ units = orch.units("foo.rd", rrd)
390
+ assert_equal ["foo.md", "foo/A.md", "foo/A__E.md"], units.map(&:path)
391
+ assert_equal <<~MD, orch.convert_unit(units[0])
392
+ ---
393
+ type: library
394
+ category: Cat
395
+ ---
396
+ 概要。
397
+
398
+ MD
399
+ assert_equal <<~MD, orch.convert_unit(units[1])
400
+ ---
401
+ library: foo
402
+ include:
403
+ - Enumerable
404
+ ---
405
+ # class A < Object
406
+
407
+ A。
408
+
409
+ MD
410
+ end
411
+ end
412
+
413
+ def test_units_keep_dual_library_entity_file_unsplit
414
+ # pathname 型(lib + 単一エンティティ兼用)は仕様が認める形なので分割しない
415
+ with_orchestrator do |orch|
416
+ rrd = "category Cat\n\n概要。\n\n= class A < Object\ninclude Enumerable\n\nA。\n"
417
+ units = orch.units("foo.rd", rrd)
418
+ assert_equal ["foo.md"], units.map(&:path)
419
+ md = orch.convert_unit(units[0])
420
+ assert_match(/\Ainclude:\n - Enumerable\n/, md[/include:.*?(?=---)/m])
421
+ end
422
+ end
423
+
424
+ def test_units_rewrite_relative_includes_in_relocated_segments
425
+ # cgi/core.rd の #@include(util.rd) 型: lib 分割でセグメントが <libname>/ 配下へ
426
+ # 移るため、相対 include は新ディレクトリから解決できるよう書き換える
427
+ with_orchestrator do |orch|
428
+ rrd = "概要。\n\n= class A < Object\ninclude Enumerable\n\n\#@include(frag)\n\n" \
429
+ "= class B < Object\nB。\n"
430
+ units = orch.units("foo.rd", rrd)
431
+ a = units.find { |u| u.path == "foo/A.md" }
432
+ assert_match(%r{\#@include\(\.\./frag\)}, a.rrd)
433
+ end
434
+ end
435
+
436
+ def test_units_inline_entities_inherit_library_gate
437
+ # gated ライブラリ(until 3.1)のインライン・エンティティは until を継承する。
438
+ # 概要部が無くてもライブラリ自体が発見から消えないよう front matter だけの
439
+ # 概要ユニットを合成する
440
+ with_orchestrator do |orch|
441
+ rrd = "= class G < Object\ninclude Enumerable\n\nG。\n\n= class G::E < StandardError\nE。\n"
442
+ units = orch.units("gated.rd", rrd)
443
+ assert_equal ["gated.md", "gated/G.md", "gated/G__E.md"], units.map(&:path)
444
+ assert_equal "---\ntype: library\nuntil: \"3.1\"\n---\n", orch.convert_unit(units[0])
445
+ assert_equal({ "library" => "gated", "until" => "3.1" }, units[1].front_matter)
446
+ end
447
+ end
448
+
449
+ def test_sublibrary_case_collision_renamed_with_name_front_matter
450
+ # x/x.rd の出力 x/x.md はメンバー出力 x/X.md と大文字小文字のみで衝突し、
451
+ # macOS 等の case-insensitive FS でチェックアウト不能になる。
452
+ # basename に .lib を挟んで回避し、ライブラリ名は name: で保つ(rdoc/rdoc 型)
453
+ with_orchestrator do |orch|
454
+ units = orch.units("x/x.rd", FILES["x/x.rd"])
455
+ assert_equal ["x/x.lib.md"], units.map(&:path)
456
+ md = orch.convert_unit(units.first)
457
+ assert md.start_with?("---\ntype: library\nname: x/x\n---\n"), md[0, 80].inspect
458
+ end
459
+ end
460
+
461
+ def test_non_colliding_library_is_not_renamed
462
+ with_orchestrator do |orch|
463
+ units = orch.units("x.rd", FILES["x.rd"])
464
+ assert_equal ["x.md"], units.map(&:path)
465
+ assert_nil units.first.front_matter["name"]
466
+ end
467
+ end
468
+
469
+ def test_renamed_library_roundtrip_restores_rd
470
+ # name: は md 側だけの情報(ファイル名衝突回避)。md→rd では消え、
471
+ # reduce の rd 到達点がそのまま復元される
472
+ with_orchestrator do |orch|
473
+ unit = orch.units("x/x.rd", FILES["x/x.rd"]).first
474
+ md = orch.convert_unit(unit)
475
+ assert_equal unit.rrd, BitClust::MarkdownToRRD.convert(md)
476
+ end
477
+ end
478
+ end