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,968 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test/unit'
4
+
5
+ require 'bitclust/rrd_to_markdown'
6
+ require 'bitclust/markdown_to_rrd'
7
+
8
+ class TestRRDToMarkdown < Test::Unit::TestCase
9
+ def convert(rrd)
10
+ BitClust::RRDToMarkdown.convert(rrd)
11
+ end
12
+
13
+ # M2: 本文バッククォートの GFM 忠実化。
14
+ # GNU 風引用 `token'(rd に適切なマークアップがなかった代替記法)は
15
+ # インラインコードスパン `token` に変換し、それ以外の生バッククォートは
16
+ # \` にエスケープする(GFM で偶発スパンにならないように)
17
+
18
+ def test_gnu_quote_becomes_code_span
19
+ assert_equal "長いオプションは `-` の代わりに `--` で始まります。\n",
20
+ convert("長いオプションは `-' の代わりに `--' で始まります。\n")
21
+ end
22
+
23
+ def test_gnu_quote_with_symbols
24
+ assert_equal "`{}` や `**/` は使用できません。\n",
25
+ convert("`{}' や `**/' は使用できません。\n")
26
+ end
27
+
28
+ def test_lone_backtick_is_escaped
29
+ # 対にならない・GNU 引用でもない生バッククォートはエスケープ
30
+ assert_equal "コード中の \\` は特別です。\n",
31
+ convert("コード中の ` は特別です。\n")
32
+ end
33
+
34
+ def test_backtick_pair_with_space_is_escaped
35
+ # 空白を含む `...' は GNU 引用とみなさない(誤検出防止)
36
+ assert_equal "\\`wget url' のように書きます。\n",
37
+ convert("`wget url' のように書きます。\n")
38
+ end
39
+
40
+ def test_no_escape_inside_emlist
41
+ rrd = "//emlist{\nout = `wget url`\n//}\n"
42
+ assert_equal "```\nout = `wget url`\n```\n", convert(rrd)
43
+ end
44
+
45
+ def test_auto_code_span_not_escaped
46
+ # __WORD__ の自動コードスパンのバッククォートはエスケープしない
47
+ assert_equal "`__FILE__` を返します。\n",
48
+ convert("__FILE__ を返します。\n")
49
+ end
50
+
51
+ def test_no_auto_span_inside_md_ref
52
+ # Delegator 型: 参照ターゲット内の __WORD__ はスパン化しない
53
+ # (保護対象は変換後の一重括弧 [m:...]。二重括弧ではない)
54
+ assert_equal "`__FILE__` と [m:Delegator#__getobj__] を参照。\n",
55
+ convert("__FILE__ と [[m:Delegator#__getobj__]] を参照。\n")
56
+ end
57
+
58
+ def test_backtick_adjacent_to_auto_span
59
+ # 生バッククォートが __WORD__ に隣接しても双方向で壊れない
60
+ rrd = "`__FILE__ を参照。\n"
61
+ md = convert(rrd)
62
+ assert_equal "\\``__FILE__` を参照。\n", md
63
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
64
+ end
65
+
66
+ def test_backtick_in_anchored_heading
67
+ # spec/pattern_matching 型: 見出し内の対バッククォートはエスケープして温存
68
+ rrd = "===[a:x] 付記B: `未定義` の振る舞いの例\n"
69
+ md = convert(rrd)
70
+ assert_equal "### 付記B: \\`未定義\\` の振る舞いの例 {#x}\n", md
71
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
72
+ end
73
+
74
+ def test_lone_backtick_heading
75
+ # symref 型: 見出しがバッククォート1文字
76
+ rrd = "===[a:ac] `\n"
77
+ md = convert(rrd)
78
+ assert_equal "### \\` {#ac}\n", md
79
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
80
+ end
81
+
82
+ def test_gnu_quote_with_backslash_not_spanned
83
+ # DOSISH 型: `\' をスパン化すると `\` が \` エスケープと曖昧になるため除外
84
+ rrd = "2 byte 目が 0x5c(`\\') である文字。\n"
85
+ md = convert(rrd)
86
+ assert_equal "2 byte 目が 0x5c(\\`\\') である文字。\n", md
87
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
88
+ end
89
+
90
+ def test_dlist_term_with_backslash_not_spanned
91
+ # symref 型: バックスラッシュを含む term はコードスパン化しない
92
+ rrd = ": xxx \\\n 説明。\n"
93
+ md = convert(rrd)
94
+ assert_equal "- **xxx \\**:\n 説明。\n", md
95
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
96
+ end
97
+
98
+ def test_tex_double_quote_not_spanned
99
+ # rdoc.rd 型: TeX 風二重引用 ``text'' はスパン化せずエスケープで温存
100
+ rrd = "二重引用符: \"text\" もしくは ``text''\n"
101
+ md = convert(rrd)
102
+ assert_equal "二重引用符: \"text\" もしくは \\`\\`text''\n", md
103
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
104
+ end
105
+
106
+ def test_dlist_description_across_directives
107
+ # pack-template 型: dd の説明は #@ 指令行を透過して続く
108
+ # (ゲート行で打ち切ると後続インデント行がコード扱いになり、
109
+ # 版解決後の描画が RDCompiler と食い違う)
110
+ rrd = ": term\n 説明。\n\#@since 2.0\n 追加説明。\n\#@end\n\n本文。\n"
111
+ md = convert(rrd)
112
+ assert_equal "- **`term`**:\n 説明。\n\#@since 2.0\n 追加説明。\n\#@end\n\n本文。\n", md
113
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
114
+ end
115
+
116
+ def test_dlist_description_across_samplecode
117
+ # pack-template 型: dd 内の #@samplecode 後の字下げ行も dd 段落継続。
118
+ # 空行スキャンが #@samplecode を透過するとコード本体の行(カラム0)を見て
119
+ # dd を打ち切り、後続段落がトップレベル pre と誤分類される。
120
+ # フェンスは CommonMark で dd にネストするよう内容カラム(2桁)へインデント
121
+ # される(新 MDCompiler の INDENTED_FENCE_RE が取り込む対象)
122
+ rrd = ": w\n\n 説明。\n\n\#@samplecode\n[0].pack(\"w\")\n\#@end\n\n なお、後続の説明。\n"
123
+ md = convert(rrd)
124
+ assert_equal "- **`w`**:\n\n 説明。\n\n ```ruby\n [0].pack(\"w\")\n ```\n\n なお、後続の説明。\n", md
125
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
126
+ end
127
+
128
+ def test_ulist_followed_by_samplecode
129
+ # news/2_5_0 型: リスト項目直後のカラム0 #@samplecode は項目の外
130
+ # (RDCompiler は ul を閉じて独立ブロックとして描画する)。
131
+ # 項目収集が生のまま素通しすると本体の #=> 行が \# エスケープされ、
132
+ # 版解決後の //emlist 内にエスケープが残って描画が壊れる
133
+ rrd = " * 項目\n\#@samplecode\nCoverage.result\n#=> [1]\n\#@end\n * 次の項目\n"
134
+ md = convert(rrd)
135
+ assert_equal " - 項目\n```ruby\nCoverage.result\n#=> [1]\n```\n - 次の項目\n", md
136
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
137
+ end
138
+
139
+ def test_param_continuation_after_gate_end
140
+ # fileutils 型: ゲートで分岐した @param の継続行が #@end の直後に来る。
141
+ # nest 0 の #@end(dd より前に開いたゲートの閉じ)も透過しないと
142
+ # 継続行がコードフェンス化され、版解決後の描画が dd 継続にならない
143
+ rrd = "\#@since 2.4.0\n@param options :a が指定できます。\n\#@else\n@param options :b が指定できます。\n\#@end\n [[ref:c:FileUtils#options]]\n"
144
+ md = convert(rrd)
145
+ assert_equal "\#@since 2.4.0\n- **param** `options` -- :a が指定できます。\n\#@else\n- **param** `options` -- :b が指定できます。\n\#@end\n [ref:c:FileUtils#options]\n", md
146
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
147
+ end
148
+
149
+ def test_gnu_quote_roundtrip
150
+ rrd = "= module M\n\n`PERMUTE', `RETURN_IN_ORDER' という順序形式と ` の話。\n"
151
+ md = convert(rrd)
152
+ assert_equal "# module M\n\n`PERMUTE`, `RETURN_IN_ORDER` という順序形式と \\` の話。\n", md
153
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
154
+ end
155
+
156
+ # Step 1: H1 ヘッダとパススルー
157
+
158
+ def test_module_header
159
+ assert_equal "# module Comparable\n",
160
+ convert("= module Comparable\n")
161
+ end
162
+
163
+ def test_class_header_with_superclass
164
+ assert_equal "# class Array < Object\n",
165
+ convert("= class Array < Object\n")
166
+ end
167
+
168
+ def test_reopen_header
169
+ assert_equal "# reopen Kernel\n",
170
+ convert("= reopen Kernel\n")
171
+ end
172
+
173
+ def test_text_passthrough
174
+ rrd = "= module Comparable\n\n説明文。\n"
175
+ expected = "# module Comparable\n\n説明文。\n"
176
+ assert_equal expected, convert(rrd)
177
+ end
178
+
179
+ def test_directive_passthrough
180
+ rrd = "= module Comparable\n\n\#@since 2.4.0\ntext\n\#@end\n"
181
+ expected = "# module Comparable\n\n\#@since 2.4.0\ntext\n\#@end\n"
182
+ assert_equal expected, convert(rrd)
183
+ end
184
+
185
+ def test_comment_passthrough
186
+ assert_equal "\#@# これはコメント\n",
187
+ convert("\#@# これはコメント\n")
188
+ end
189
+
190
+ def test_include_passthrough
191
+ assert_equal "\#@include(io/buffer.md)\n",
192
+ convert("\#@include(io/buffer.md)\n")
193
+ end
194
+
195
+ # Step 2: H2 セクションヘッダ
196
+
197
+ def test_h2_instance_methods
198
+ assert_equal "## Instance Methods\n",
199
+ convert("== Instance Methods\n")
200
+ end
201
+
202
+ def test_h2_class_methods
203
+ assert_equal "## Class Methods\n",
204
+ convert("== Class Methods\n")
205
+ end
206
+
207
+ def test_h2_constants
208
+ assert_equal "## Constants\n",
209
+ convert("== Constants\n")
210
+ end
211
+
212
+ # Step 3: メソッドシグネチャ + コードブロック
213
+
214
+ def test_instance_method_signature
215
+ # B1: 空白を保持(正規化しない)
216
+ assert_equal "### def ==(other) -> bool\n",
217
+ convert("--- ==(other) -> bool\n")
218
+ end
219
+
220
+ def test_method_signature_whitespace_preserved
221
+ assert_equal "### def [](nth) -> object | nil\n",
222
+ convert("--- [](nth) -> object | nil\n")
223
+ end
224
+
225
+ def test_method_signature_with_block
226
+ assert_equal "### def new(size) {|index| ... } -> Array\n",
227
+ convert("--- new(size) {|index| ... } -> Array\n")
228
+ end
229
+
230
+ def test_multiple_signatures
231
+ rrd = "--- [](nth) -> object | nil\n--- at(nth) -> object | nil\n"
232
+ expected = "### def [](nth) -> object | nil\n### def at(nth) -> object | nil\n"
233
+ assert_equal expected, convert(rrd)
234
+ end
235
+
236
+ def test_code_block_with_label
237
+ rrd = "\#@samplecode 例\n1 == 1 # => true\n\#@end\n"
238
+ expected = "```ruby title=\"例\"\n1 == 1 # => true\n```\n"
239
+ assert_equal expected, convert(rrd)
240
+ end
241
+
242
+ def test_code_block_without_label
243
+ rrd = "\#@samplecode\n1 == 1\n\#@end\n"
244
+ expected = "```ruby\n1 == 1\n```\n"
245
+ assert_equal expected, convert(rrd)
246
+ end
247
+
248
+ def test_samplecode_label_with_backslash
249
+ rrd = "\#@samplecode \"\\n\" を含む例\n1\n\#@end\n"
250
+ expected = "```ruby title=\"\\\"\\\\n\\\" を含む例\"\n1\n```\n"
251
+ assert_equal expected, convert(rrd)
252
+ end
253
+
254
+ def test_emlist_with_caption_and_lang
255
+ rrd = "//emlist[例][ruby]{\nputs 'hello'\n//}\n"
256
+ expected = "```ruby title=\"例\"\nputs 'hello'\n```\n"
257
+ assert_equal expected, convert(rrd)
258
+ end
259
+
260
+ def test_emlist_without_caption_with_lang
261
+ rrd = "//emlist[][sh]{\necho hello\n//}\n"
262
+ expected = "```sh\necho hello\n```\n"
263
+ assert_equal expected, convert(rrd)
264
+ end
265
+
266
+ def test_emlist_without_caption_without_lang
267
+ rrd = "//emlist{\nplain text\n//}\n"
268
+ expected = "```\nplain text\n```\n"
269
+ assert_equal expected, convert(rrd)
270
+ end
271
+
272
+ def test_emlist_caption_with_double_quote
273
+ rrd = "//emlist[He said \"hello\"][ruby]{\ncode\n//}\n"
274
+ expected = "```ruby title=\"He said \\\"hello\\\"\"\ncode\n```\n"
275
+ assert_equal expected, convert(rrd)
276
+ end
277
+
278
+ # Step 4: ClassName. → self. 変換
279
+
280
+ def test_class_method_keeps_classname
281
+ rrd = "= class Array < Object\n\n--- Array.try_convert(obj) -> Array | nil\n"
282
+ result = convert(rrd)
283
+ assert_match(/### def Array\.try_convert\(obj\)/, result)
284
+ end
285
+
286
+ def test_module_method_keeps_classname
287
+ rrd = "= module Benchmark\n\n--- Benchmark.measure(label) -> Benchmark::Tms\n"
288
+ result = convert(rrd)
289
+ assert_match(/### def Benchmark\.measure\(label\)/, result)
290
+ end
291
+
292
+ # Step 5: 定数・グローバル変数
293
+
294
+ def test_constant_signature
295
+ assert_equal "### const CR -> String\n",
296
+ convert("--- CR -> String\n")
297
+ end
298
+
299
+ def test_constant_without_arrow
300
+ assert_equal "### const START\n",
301
+ convert("--- START\n")
302
+ end
303
+
304
+ def test_global_variable_signature
305
+ assert_equal "### gvar $DEBUG -> bool\n",
306
+ convert("--- $DEBUG -> bool\n")
307
+ end
308
+
309
+ # module_function
310
+
311
+ def test_module_function_signature
312
+ rrd = "== Module Functions\n\n--- measure(label) -> Benchmark::Tms\n"
313
+ result = convert(rrd)
314
+ assert_match(/### module_function def measure\(label\)/, result)
315
+ end
316
+
317
+ # ClassName.method はそのまま保持(返り値なしパターン)
318
+
319
+ def test_class_method_keeps_classname_without_nil
320
+ rrd = "= class Array < Object\n\n--- Array.try_convert(obj) -> Array\n"
321
+ result = convert(rrd)
322
+ assert_match(/### def Array\.try_convert\(obj\)/, result)
323
+ end
324
+
325
+ # 大文字始まりメソッド(( がある)
326
+
327
+ def test_uppercase_method_with_parens
328
+ assert_equal "### def DEBUG=(val)\n",
329
+ convert("--- DEBUG=(val)\n")
330
+ end
331
+
332
+ # .# → ? 参照変換
333
+
334
+ def test_module_function_ref
335
+ assert_equal "テスト [m:Kernel?.open]\n",
336
+ convert("テスト [[m:Kernel.#open]]\n")
337
+ end
338
+
339
+ # Step 6: @param / @return / @raise
340
+
341
+ def test_param
342
+ assert_equal "- **param** `other` -- 自身と比較したいオブジェクトを指定します。\n",
343
+ convert("@param other 自身と比較したいオブジェクトを指定します。\n")
344
+ end
345
+
346
+ def test_raise
347
+ assert_equal "- **raise** `ArgumentError` -- <=> が nil を返したときに発生します。\n",
348
+ convert("@raise ArgumentError <=> が nil を返したときに発生します。\n")
349
+ end
350
+
351
+ def test_return
352
+ assert_equal "- **return** -- 説明\n",
353
+ convert("@return 説明\n")
354
+ end
355
+
356
+ def test_param_with_inline_ref
357
+ assert_equal "- **param** `obj` -- [c:Range] オブジェクト\n",
358
+ convert("@param obj [[c:Range]] オブジェクト\n")
359
+ end
360
+
361
+ def test_raise_with_inline_ref
362
+ assert_equal "- **raise** `TypeError` -- [c:String] 以外\n",
363
+ convert("@raise TypeError [[c:String]] 以外\n")
364
+ end
365
+
366
+ def test_return_with_inline_ref
367
+ assert_equal "- **return** -- [c:Array] を返す\n",
368
+ convert("@return [[c:Array]] を返す\n")
369
+ end
370
+
371
+ def test_return_alignment_spaces_preserved
372
+ # B2: @return の整列空白を保持
373
+ assert_equal "- **return** -- 説明\n",
374
+ convert("@return 説明\n")
375
+ end
376
+
377
+ def test_metadata_continuation_line
378
+ rrd = "@raise ArgumentError self <=> min か、self <=> max が nil を返\n したときに発生します。\n"
379
+ expected = "- **raise** `ArgumentError` -- self <=> min か、self <=> max が nil を返\n したときに発生します。\n"
380
+ assert_equal expected, convert(rrd)
381
+ end
382
+
383
+ # Step 7: @see
384
+
385
+ def test_see_single
386
+ # B3: @see は [[→[ 置換のみ、区切りは保持
387
+ assert_equal "- **SEE** [m:String#-@]\n",
388
+ convert("@see [[m:String#-@]]\n")
389
+ end
390
+
391
+ def test_see_multiple_with_comma_space
392
+ assert_equal "- **SEE** [m:A], [m:B]\n",
393
+ convert("@see [[m:A]], [[m:B]]\n")
394
+ end
395
+
396
+ def test_see_multiple_without_comma_space
397
+ # B3: カンマ後スペースなしも保持
398
+ assert_equal "- **SEE** [m:A],[m:B]\n",
399
+ convert("@see [[m:A]],[[m:B]]\n")
400
+ end
401
+
402
+ def test_see_class_ref
403
+ assert_equal "- **SEE** [c:Array]\n",
404
+ convert("@see [[c:Array]]\n")
405
+ end
406
+
407
+ def test_see_doc_ref
408
+ assert_equal "- **SEE** [d:spec/m17n]\n",
409
+ convert("@see [[d:spec/m17n]]\n")
410
+ end
411
+
412
+ # Step 8: YAML front matter
413
+
414
+ def test_include_to_front_matter
415
+ rrd = "= class Array < Object\ninclude Enumerable\n\n説明\n"
416
+ expected = "---\ninclude:\n - Enumerable\n---\n# class Array < Object\n\n説明\n"
417
+ assert_equal expected, convert(rrd)
418
+ end
419
+
420
+ def test_multiple_includes_to_front_matter
421
+ rrd = "= class Array < Object\ninclude Enumerable\ninclude Comparable\n\n説明\n"
422
+ expected = "---\ninclude:\n - Enumerable\n - Comparable\n---\n# class Array < Object\n\n説明\n"
423
+ assert_equal expected, convert(rrd)
424
+ end
425
+
426
+ def test_extend_to_front_matter
427
+ rrd = "= class Foo < Object\nextend Forwardable\n"
428
+ expected = "---\nextend:\n - Forwardable\n---\n# class Foo < Object\n"
429
+ assert_equal expected, convert(rrd)
430
+ end
431
+
432
+ def test_alias_to_front_matter
433
+ rrd = "= class Integer < Numeric\nalias Fixnum\n"
434
+ expected = "---\nalias:\n - Fixnum\n---\n# class Integer < Numeric\n"
435
+ assert_equal expected, convert(rrd)
436
+ end
437
+
438
+ def test_multi_entity_file_keeps_relations_in_body
439
+ # 複数エンティティを含むファイルは帰属が曖昧なため front matter 化しない(分割前の安全策)
440
+ rrd = "= object ARGF < ARGF.class\n\n説明1\n\n= class ARGF.class < Object\ninclude Enumerable\n\n説明2\n"
441
+ result = convert(rrd)
442
+ refute_match(/\A---/, result)
443
+ assert_match(/# class ARGF\.class < Object\ninclude Enumerable/, result)
444
+ end
445
+
446
+ def test_versioned_alias_to_front_matter
447
+ # 版条件(#@)で囲まれたヘッダ関係は front matter 内に #@ を挟んで表現する(§1.6)
448
+ rrd = "= class Integer < Numeric\n\n\#@until 3.2\nalias Fixnum\nalias Bignum\n\#@end\n\n説明\n"
449
+ expected = "---\nalias:\n\#@until 3.2\n - Fixnum\n - Bignum\n\#@end\n---\n# class Integer < Numeric\n\n説明\n"
450
+ assert_equal expected, convert(rrd)
451
+ end
452
+
453
+ # Step 8.3: doc ツリー由来のリテラルエスケープ(refm/doc 対応)
454
+ # 旧 news 等には「参照や見出しに見えるリテラル本文」があり、
455
+ # md 側でエスケープしないと md→rd の復元で誤変換される。
456
+
457
+ def test_literal_bare_ref_lookalike_is_escaped
458
+ # news/1_9_0: [ruby-talk:198440] はリテラル本文(参照ではない)
459
+ rrd = "= class A < Object\n\n無名モジュールの変更\n\\[ruby-talk:198440] を参照。\n"
460
+ md = convert("= class A < Object\n\n無名モジュールの変更\n[ruby-talk:198440] を参照。\n")
461
+ assert_match(/\\\[ruby-talk:198440\]/, md)
462
+ end
463
+
464
+ def test_real_refs_are_not_escaped
465
+ md = convert("= class A < Object\n\n[[m:Array#each]] と [[c:String]] を参照。\n")
466
+ assert_match(/\[m:Array#each\] と \[c:String\]/, md)
467
+ refute_match(/\\\[/, md)
468
+ end
469
+
470
+ def test_leading_hash_line_is_escaped
471
+ # news/1.6.0: 行頭 # のリテラル本文(コメントアウトされた変更履歴)
472
+ md = convert("= class A < Object\n\n\# : 2002-08-01 IO#read\n\# 本文。\n")
473
+ assert_match(/^\\# : 2002-08-01 IO#read$/, md)
474
+ assert_match(/^\\# 本文。$/, md)
475
+ end
476
+
477
+ def test_anchored_h1
478
+ # spec/rubycmd.rd: =[a:ruby] Rubyの起動
479
+ assert_equal "# Rubyの起動 {#ruby}\n", convert("=[a:ruby] Rubyの起動\n")
480
+ end
481
+
482
+ def test_heading_trailing_space_is_preserved
483
+ # news/1.8.2: 「=== 2004-12-06 」(末尾スペース)
484
+ assert_equal "### 2004-12-06 \n", convert("=== 2004-12-06 \n")
485
+ end
486
+
487
+ # Step 7.5: @param 継続の貪欲さ(RDCompiler の dd_without_p と同規則)
488
+ # 空白を含む行(" \n" や字下げの例示ブロック)は @param 説明の継続。
489
+ # 完全な空行("\n")でのみ停止する(CGI::HtmlExtension#popup_menu 型)。
490
+
491
+ def test_param_continuation_spans_whitespace_only_lines
492
+ rrd = "= class A < Object\n== Instance Methods\n--- m(v) -> String\n\n" \
493
+ "@param v 説明。\n \n 例:\n code\n\n本文。\n"
494
+ md = convert(rrd)
495
+ assert_match(/-- 説明。\n \n 例:\n code\n\n本文。/, md)
496
+ refute_match(/```/, md) # 例示ブロックはフェンス化せず継続のまま
497
+ end
498
+
499
+ # Step 7.6: RDCompiler の文脈解釈との整合(描画等価のための変換制約)
500
+ # RDCompiler は (a) dlist の説明を空行を跨いでインデントが続く限り dd とし
501
+ # (リスト風の行もテキスト)、(b) 段落中の「: 」行を段落の継続とし、
502
+ # (c) リスト項目直後のインデント行を深さ不問で項目の継続とする。
503
+ # 変換器はこの解釈を保存する(構造を変えると描画が変わるため)。
504
+
505
+ def test_dlist_description_spans_blank_lines_keeping_list_like_text
506
+ rrd = ": term\n 説明。\n\n * 項目風テキスト1\n * 項目風テキスト2\n\n本文。\n"
507
+ md = convert(rrd)
508
+ assert_match(/- \*\*`term`\*\*:\n 説明。\n\n \* 項目風テキスト1\n \* 項目風テキスト2\n\n本文。/, md)
509
+ end
510
+
511
+ def test_colon_line_inside_paragraph_stays_text
512
+ # news/1.8.5 型: テキスト行直後の「: 」行は RDCompiler では段落の継続
513
+ rrd = "以下のメソッドが追加されました。\n: TCPServer#accept_nonblock [new]\n\n本文。\n"
514
+ md = convert(rrd)
515
+ assert_match(/^: TCPServer#accept_nonblock \[new\]$/, md)
516
+ refute_match(/\*\*/, md)
517
+ end
518
+
519
+ def test_list_continuation_shallower_than_content_indent
520
+ # news/2_6_0 型: 項目より浅いインデントの行も RDCompiler では項目の継続
521
+ rrd = " * 項目の一行目が長くて\n 折り返した継続行。\n\n本文。\n"
522
+ md = convert(rrd)
523
+ assert_match(/ - 項目の一行目が長くて\n 折り返した継続行。\n\n本文。/, md)
524
+ refute_match(/`/, md)
525
+ end
526
+
527
+ # Step 8.35: メタデータ領域の #@# コメント(irb.rd 対応)
528
+ # メタ行の間に挟まる #@# は front matter 内に保持する(YAML コメントになる)。
529
+ # メタ領域の後・本文の前の #@#(rss.rd 型)は従来どおり body に残す。
530
+
531
+ def test_leading_comment_in_metadata_to_front_matter
532
+ # irb.rd: ファイル先頭の #@# Author コメント → category の直前に保持
533
+ rrd = "\#@# Author: Keiju\n\ncategory Development\n\n本文。\n"
534
+ expected = "---\n\#@# Author: Keiju\ncategory: Development\n---\n本文。\n"
535
+ assert_equal expected, convert(rrd)
536
+ end
537
+
538
+ def test_comment_inside_require_block_to_front_matter
539
+ # irb.rd: require 群の途中の #@# 注記 → require ブロック行として保持
540
+ rrd = "category Development\n\n\#@until 2.7.0\nrequire a\n\#@end\nrequire b\n" \
541
+ "\#@# note\nrequire c\n\n本文。\n"
542
+ expected = "---\ncategory: Development\nrequire:\n\#@until 2.7.0\n - a\n\#@end\n" \
543
+ " - b\n\#@# note\n - c\n---\n本文。\n"
544
+ assert_equal expected, convert(rrd)
545
+ end
546
+
547
+ def test_comment_directly_above_require_joins_the_block
548
+ # irb/workspace.rd: 先頭 #@# の直後に空行なしで require が続く
549
+ # (コメントはその require の説明)→ require ブロックの先頭行として保持
550
+ rrd = "\#@# CONTEXT_MODE 2 の場合のみ。\nrequire irb/ws-for-case-2\n\n本文。\n"
551
+ expected = "---\nrequire:\n\#@# CONTEXT_MODE 2 の場合のみ。\n - irb/ws-for-case-2\n---\n本文。\n"
552
+ assert_equal expected, convert(rrd)
553
+ end
554
+
555
+ def test_trailing_comment_before_body_stays_in_body
556
+ # rss.rd 型: メタ確定後の #@# は本文の前置き → body 据え置き(既存動作の維持)
557
+ rrd = "category FileFormat\n\n\#@# = rss\n\n説明。\n"
558
+ expected = "---\ncategory: FileFormat\n---\n\#@# = rss\n\n説明。\n"
559
+ assert_equal expected, convert(rrd)
560
+ end
561
+
562
+ # Step 8.4: メタデータ領域の部分コミット
563
+ # メタデータ(category/require/sublibrary)の後に版分岐つき散文や #@# コメントが
564
+ # 続く場合、nest==0 の空行チェックポイントまでをメタデータとして確定し、
565
+ # 残りは body に渡す(set.rd / thread.rd / rss.rd パターン)。
566
+
567
+ def test_category_lifts_before_versioned_prose
568
+ # set.rd: category Math + 空行 + 版分岐つき散文
569
+ rrd = "category Math\n\n\#@since 3.0\n新しい説明\n\#@else\n古い説明\n\#@end\n\n本文。\n"
570
+ expected = "---\ncategory: Math\n---\n" \
571
+ "\#@since 3.0\n新しい説明\n\#@else\n古い説明\n\#@end\n\n本文。\n"
572
+ assert_equal expected, convert(rrd)
573
+ end
574
+
575
+ def test_category_lifts_before_preprocessor_comment
576
+ # rss.rd: category FileFormat + 空行 + #@# コメント
577
+ rrd = "category FileFormat\n\n\#@# = rss\n\n説明。\n"
578
+ expected = "---\ncategory: FileFormat\n---\n\#@# = rss\n\n説明。\n"
579
+ assert_equal expected, convert(rrd)
580
+ end
581
+
582
+ def test_metadata_without_blank_checkpoint_stays_in_body
583
+ # 空行チェックポイントが無い(category 直後にゲート)場合は従来どおり据え置き
584
+ # (md→rd の再生成が category の後に空行を足すため、コミットすると一致しない)
585
+ rrd = "category Math\n\#@since 3.0\n説明\n\#@end\n"
586
+ assert_equal "category Math\n\#@since 3.0\n説明\n\#@end\n", convert(rrd)
587
+ end
588
+
589
+ # Step 8.5: extra front matter 注入(クロスファイル・オーケストレータ用)
590
+ # library 所属・構造 since/until はファイル単体からは分からないため、
591
+ # include グラフを解析したオーケストレータが注入する。
592
+
593
+ def test_extra_front_matter_library
594
+ rrd = "= class Bar < Object\n\n説明\n"
595
+ expected = "---\nlibrary: foo\n---\n# class Bar < Object\n\n説明\n"
596
+ assert_equal expected,
597
+ BitClust::RRDToMarkdown.convert(rrd, extra_front_matter: { "library" => "foo" })
598
+ end
599
+
600
+ def test_extra_front_matter_ordering_with_collected_relations
601
+ # §1.7 の順序: library → include/extend/alias → since/until
602
+ rrd = "= class Bar < Object\ninclude Enumerable\n\n説明\n"
603
+ expected = "---\nlibrary: foo\ninclude:\n - Enumerable\nsince: \"3.2\"\n---\n" \
604
+ "# class Bar < Object\n\n説明\n"
605
+ assert_equal expected,
606
+ BitClust::RRDToMarkdown.convert(rrd,
607
+ extra_front_matter: { "library" => "foo", "since" => "3.2" })
608
+ end
609
+
610
+ def test_extra_front_matter_accepts_symbol_keys
611
+ # IncludeGraph::Scope#gate は {since:, until:} のシンボルキーを返す
612
+ rrd = "= class Bar < Object\n"
613
+ expected = "---\nlibrary: foo\nuntil: \"4.0\"\n---\n# class Bar < Object\n"
614
+ assert_equal expected,
615
+ BitClust::RRDToMarkdown.convert(rrd,
616
+ extra_front_matter: { library: "foo", until: "4.0" })
617
+ end
618
+
619
+ def test_extra_front_matter_gated_library_list
620
+ # 多重所属(IncludeGraph#front_matter_map のリスト形式)は
621
+ # ゲート付きリストとして emit する(MARKUP_SPEC §1.2/§1.6)
622
+ rrd = "= class Mutex < Object\n\n説明\n"
623
+ expected = "---\n" \
624
+ "library:\n" \
625
+ " - _builtin\n" \
626
+ "\#@until 1.9.1\n" \
627
+ " - thread\n" \
628
+ "\#@end\n" \
629
+ "---\n" \
630
+ "# class Mutex < Object\n\n説明\n"
631
+ assert_equal expected,
632
+ BitClust::RRDToMarkdown.convert(rrd, extra_front_matter: {
633
+ "library" => [
634
+ { "name" => "_builtin" },
635
+ { "name" => "thread", "until" => "1.9.1" }
636
+ ] })
637
+ end
638
+
639
+ def test_extra_front_matter_gated_library_list_with_since_and_both_bounds
640
+ rrd = "= class Foo < Object\n"
641
+ expected = "---\n" \
642
+ "library:\n" \
643
+ "\#@since 2.3.0\n" \
644
+ " - _builtin\n" \
645
+ "\#@end\n" \
646
+ "\#@since 1.9.1\n" \
647
+ "\#@until 2.3.0\n" \
648
+ " - thread\n" \
649
+ "\#@end\n" \
650
+ "\#@end\n" \
651
+ "---\n" \
652
+ "# class Foo < Object\n"
653
+ assert_equal expected,
654
+ BitClust::RRDToMarkdown.convert(rrd, extra_front_matter: {
655
+ "library" => [
656
+ { "name" => "_builtin", "since" => "2.3.0" },
657
+ { "name" => "thread", "since" => "1.9.1", "until" => "2.3.0" }
658
+ ] })
659
+ end
660
+
661
+ def test_gated_metadata_blocks_of_different_kinds
662
+ # regate_metadata の出力形: require と sublibrary が別々の単一種ゲート
663
+ # ブロックに分かれている(rubygems.rd の据え置きゲート)。
664
+ # それぞれの front matter キーへゲートごと振り分ける
665
+ rrd = "\#@since 1.9.1\nrequire a\nrequire b\n\#@end\n\n" \
666
+ "\#@since 1.9.1\nsublibrary s\n\#@end\n\n" \
667
+ "\#@since 1.9.1\n本文。\n\#@end\n"
668
+ expected = "---\n" \
669
+ "require:\n" \
670
+ "\#@since 1.9.1\n" \
671
+ " - a\n" \
672
+ " - b\n" \
673
+ "\#@end\n" \
674
+ "sublibrary:\n" \
675
+ "\#@since 1.9.1\n" \
676
+ " - s\n" \
677
+ "\#@end\n" \
678
+ "---\n" \
679
+ "\#@since 1.9.1\n本文。\n\#@end\n"
680
+ assert_equal expected, BitClust::RRDToMarkdown.convert(rrd)
681
+ end
682
+
683
+ def test_gated_category_becomes_gated_scalar
684
+ # cmath.rd の据え置きゲート: category はスカラのままゲート行で挟む
685
+ # (値の差し替えではなく単一値の存在ゲートなので生 YAML でも重複キーにならない)
686
+ rrd = "\#@since 1.9.1\ncategory Math\n\#@end\n\n" \
687
+ "\#@since 1.9.1\n本文。\n\#@end\n"
688
+ expected = "---\n" \
689
+ "\#@since 1.9.1\n" \
690
+ "category: Math\n" \
691
+ "\#@end\n" \
692
+ "---\n" \
693
+ "\#@since 1.9.1\n本文。\n\#@end\n"
694
+ assert_equal expected, BitClust::RRDToMarkdown.convert(rrd)
695
+ end
696
+
697
+ def test_extra_front_matter_on_multi_entity_file
698
+ # library はファイル単位の情報なので、マルチエンティティでも曖昧なく注入できる
699
+ # (ファイル内の全エンティティは同一ライブラリ所属。ヘッダ関係は body 据え置きのまま)
700
+ rrd = "= object ARGF < ARGF.class\n\n説明1\n\n= class ARGF.class < Object\ninclude Enumerable\n\n説明2\n"
701
+ result = BitClust::RRDToMarkdown.convert(rrd,
702
+ extra_front_matter: { "library" => "_builtin" })
703
+ assert result.start_with?("---\nlibrary: _builtin\n---\n"), result[0, 60].inspect
704
+ assert_match(/include Enumerable/, result)
705
+ end
706
+
707
+ def test_extra_front_matter_rejects_unknown_keys
708
+ assert_raise(ArgumentError) do
709
+ BitClust::RRDToMarkdown.convert("= class Bar < Object\n",
710
+ extra_front_matter: { "libary" => "typo" })
711
+ end
712
+ end
713
+
714
+ def test_extra_front_matter_roundtrip_drops_injected_keys
715
+ # 注入キーはオーケストレータ由来の横断情報なので、md→rd では body に現れず
716
+ # 元の RD がそのまま復元される
717
+ require 'bitclust/markdown_to_rrd'
718
+ rrd = "= class Bar < Object\ninclude Enumerable\n\n説明\n"
719
+ md = BitClust::RRDToMarkdown.convert(rrd,
720
+ extra_front_matter: { "library" => "foo", "since" => "3.2" })
721
+ assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
722
+ end
723
+
724
+ def test_extra_front_matter_name_emitted_after_type
725
+ # ファイル名の大文字小文字衝突回避で改名されたライブラリファイル
726
+ # (rdoc/rdoc.lib.md)の名前保持用。順序は type → name(§1.7)
727
+ rrd = "x/x の説明。\n"
728
+ expected = "---\ntype: library\nname: x/x\n---\nx/x の説明。\n"
729
+ assert_equal expected,
730
+ BitClust::RRDToMarkdown.convert(rrd,
731
+ extra_front_matter: { "type" => "library", "name" => "x/x" })
732
+ end
733
+
734
+ def test_mixed_gated_alias_and_plain_include_to_front_matter
735
+ # net/Net__HTTPServerException(分割後): 版条件つき alias と素の include の混在。
736
+ # #@ ブロックごとに単一種なら front matter 化できる
737
+ rrd = "= class A < B\n\#@since 2.6.0\nalias C\n\#@end\ninclude D\n\n説明\n"
738
+ expected = "---\ninclude:\n - D\nalias:\n\#@since 2.6.0\n - C\n\#@end\n---\n" \
739
+ "# class A < B\n\n説明\n"
740
+ assert_equal expected, convert(rrd)
741
+ end
742
+
743
+ def test_mixed_kinds_within_one_gate_block_stay_in_body
744
+ # 1つの #@ ブロック内に複数種はデータ上存在しないので据え置きのまま
745
+ rrd = "= class A < B\n\#@since 2.6.0\nalias C\ninclude D\n\#@end\n\n説明\n"
746
+ assert_equal "# class A < B\n\#@since 2.6.0\nalias C\ninclude D\n\#@end\n\n説明\n",
747
+ convert(rrd)
748
+ end
749
+
750
+ def test_versioned_include_if_to_front_matter
751
+ rrd = "= class File < IO\n\#@if (version < \"1.8.0\")\ninclude File::Constants\n\#@end\n\n説明\n"
752
+ expected = "---\ninclude:\n\#@if (version < \"1.8.0\")\n - File::Constants\n\#@end\n---\n# class File < IO\n\n説明\n"
753
+ assert_equal expected, convert(rrd)
754
+ end
755
+
756
+ def test_versioned_prose_not_treated_as_header_relation
757
+ # #@ が本文(関係でない)を包む場合はヘッダ関係扱いしない
758
+ rrd = "= module Foo\n\n\#@since 3.0\n本文\n\#@end\n"
759
+ expected = "# module Foo\n\n\#@since 3.0\n本文\n\#@end\n"
760
+ assert_equal expected, convert(rrd)
761
+ end
762
+
763
+ def test_front_matter_category
764
+ rrd = "category Network\n\nライブラリの説明\n"
765
+ result = convert(rrd)
766
+ assert_match(/\A---\ncategory: Network\n---\n/, result)
767
+ assert_match(/ライブラリの説明/, result)
768
+ end
769
+
770
+ def test_front_matter_require
771
+ rrd = "require cgi/core\nrequire cgi/cookie\n\nライブラリの説明\n"
772
+ result = convert(rrd)
773
+ assert_match(/require:\n - cgi\/core\n - cgi\/cookie\n/, result)
774
+ end
775
+
776
+ def test_front_matter_sublibrary
777
+ rrd = "sublibrary json/ext\n\nライブラリの説明\n"
778
+ result = convert(rrd)
779
+ assert_match(/sublibrary:\n - json\/ext\n/, result)
780
+ end
781
+
782
+ def test_front_matter_category_and_require
783
+ rrd = "category Network\n\nrequire socket\n\nライブラリの説明\n"
784
+ result = convert(rrd)
785
+ assert_match(/\A---\ncategory: Network\nrequire:\n - socket\n---\n/, result)
786
+ end
787
+
788
+ def test_versioned_require_to_front_matter
789
+ rrd = "category Network\n\n\#@since 1.9.1\nrequire cgi/core\nrequire cgi/cookie\n\#@end\n\n説明\n"
790
+ expected = "---\ncategory: Network\nrequire:\n\#@since 1.9.1\n - cgi/core\n - cgi/cookie\n\#@end\n---\n説明\n"
791
+ assert_equal expected, convert(rrd)
792
+ end
793
+
794
+ def test_file_spanning_version_gate_metadata_stays_in_body
795
+ # ファイル全体を包む #@(構造的ゲート)はメタを front matter にしない(項目1で対応)
796
+ rrd = "\#@since 1.9.1\n\ncategory Math\n\n説明\n\#@end\n"
797
+ result = convert(rrd)
798
+ refute_match(/\A---/, result)
799
+ assert_match(/\A\#@since 1.9.1\n/, result)
800
+ end
801
+
802
+ def test_no_front_matter_when_no_metadata
803
+ rrd = "= module Comparable\n\n説明\n"
804
+ result = convert(rrd)
805
+ assert_match(/\A# module Comparable\n/, result)
806
+ refute_match(/\A---/, result)
807
+ end
808
+
809
+ def test_include_after_blank_line_to_front_matter
810
+ rrd = "= class String < Object\n\ninclude Comparable\n\n説明\n"
811
+ expected = "---\ninclude:\n - Comparable\n---\n# class String < Object\n\n説明\n"
812
+ assert_equal expected, convert(rrd)
813
+ end
814
+
815
+ # Step 9: アンカー付き見出し・H4
816
+
817
+ def test_h3_with_anchor
818
+ assert_equal "### 破壊的な変更 {#mutable}\n",
819
+ convert("===[a:mutable] 破壊的な変更\n")
820
+ end
821
+
822
+ def test_h4_heading
823
+ assert_equal "#### 小見出し\n",
824
+ convert("==== 小見出し\n")
825
+ end
826
+
827
+ def test_h3_without_anchor
828
+ assert_equal "### 読み込み\n",
829
+ convert("=== 読み込み\n")
830
+ end
831
+
832
+ # Step 10: クロスリファレンス(インライン)
833
+
834
+ def test_inline_class_ref
835
+ assert_equal "[c:String] を参照。\n",
836
+ convert("[[c:String]] を参照。\n")
837
+ end
838
+
839
+ def test_inline_method_ref
840
+ assert_equal "[m:Array#each] と同じ。\n",
841
+ convert("[[m:Array#each]] と同じ。\n")
842
+ end
843
+
844
+ def test_inline_multiple_refs
845
+ assert_equal "[c:Array] と [c:Hash] を使う。\n",
846
+ convert("[[c:Array]] と [[c:Hash]] を使う。\n")
847
+ end
848
+
849
+ def test_inline_bracket_method_ref
850
+ # A2: [[m:Hash#[] ]] → [m:Hash#\[\]]
851
+ assert_equal "- **SEE** [m:Hash#\\[\\]]\n",
852
+ convert("@see [[m:Hash#[] ]]\n")
853
+ end
854
+
855
+ def test_inline_ref_with_trailing_bracket
856
+ # [[m:Math::PI]]] → [m:Math::PI]]
857
+ assert_equal "ら [-self, [m:Math::PI]] を返します。\n",
858
+ convert("ら [-self, [[m:Math::PI]]] を返します。\n")
859
+ end
860
+
861
+ def test_inline_ref_not_in_code_block
862
+ rrd = "\#@samplecode\n[[c:String]]\n\#@end\n"
863
+ expected = "```ruby\n[[c:String]]\n```\n"
864
+ assert_equal expected, convert(rrd)
865
+ end
866
+
867
+ # Step 11: リスト変換
868
+
869
+ def test_unordered_list
870
+ # B4: 元のインデント幅を保持
871
+ rrd = " * item1\n * item2\n"
872
+ expected = " - item1\n - item2\n"
873
+ assert_equal expected, convert(rrd)
874
+ end
875
+
876
+ def test_unordered_list_2space_indent
877
+ rrd = " * item1\n * item2\n"
878
+ expected = " - item1\n - item2\n"
879
+ assert_equal expected, convert(rrd)
880
+ end
881
+
882
+ def test_ordered_list
883
+ rrd = " (1) 最初\n (2) 次\n (3) 最後\n"
884
+ expected = " 1. 最初\n 2. 次\n 3. 最後\n"
885
+ assert_equal expected, convert(rrd)
886
+ end
887
+
888
+ def test_text_number_to_bold
889
+ rrd = "1. テキスト\n"
890
+ expected = "**1.** テキスト\n"
891
+ assert_equal expected, convert(rrd)
892
+ end
893
+
894
+ # C1: インデントコードブロック +3
895
+
896
+ def test_indented_code_block
897
+ rrd = "説明\n code line 1\n code line 2\n\n次の段落\n"
898
+ expected = "説明\n`````\ncode line 1\ncode line 2\n`````\n\n次の段落\n"
899
+ assert_equal expected, convert(rrd)
900
+ end
901
+
902
+ def test_indented_code_block_8spaces
903
+ rrd = "説明\n code\n\n"
904
+ expected = "説明\n```````````\ncode\n```````````\n\n"
905
+ assert_equal expected, convert(rrd)
906
+ end
907
+
908
+ # Step 12: 定義リスト
909
+
910
+ def test_definition_list
911
+ rrd = ": type\n Content-Type header\n"
912
+ expected = "- **`type`**:\n Content-Type header\n"
913
+ assert_equal expected, convert(rrd)
914
+ end
915
+
916
+ def test_definition_list_multiple
917
+ rrd = ": type\n Content-Type header\n: charset\n Character set\n"
918
+ expected = "- **`type`**:\n Content-Type header\n- **`charset`**:\n Character set\n"
919
+ assert_equal expected, convert(rrd)
920
+ end
921
+
922
+ def test_definition_list_multiline
923
+ rrd = ": [ ]\n 鈎括弧内のいずれかの文字と一致します。- でつな\n がれた文字は範囲を表します。\n"
924
+ expected = "- **`[ ]`**:\n 鈎括弧内のいずれかの文字と一致します。- でつな\n がれた文字は範囲を表します。\n"
925
+ assert_equal expected, convert(rrd)
926
+ end
927
+
928
+ # __WORD__ コードスパン変換
929
+
930
+ def test_add_code_spans_basic
931
+ assert_match(/`__END__`/, convert("__END__ を使う\n"))
932
+ end
933
+
934
+ # Step 13: 統合テスト — Comparable ラウンドトリップ
935
+
936
+ def test_comparable_roundtrip
937
+ rrd_path = File.expand_path('../../doctree/refm/api/src/_builtin/Comparable', __dir__)
938
+ omit 'doctree not found' unless File.exist?(rrd_path)
939
+
940
+ require 'bitclust/markdown_to_rrd'
941
+ require 'bitclust/methoddatabase'
942
+ require 'bitclust/rrdparser'
943
+
944
+ params = {"version" => "3.4"}
945
+
946
+ # Original → parse
947
+ db1 = BitClust::MethodDatabase.dummy(params)
948
+ lib1 = BitClust::RRDParser.new(db1).parse_file(rrd_path, "_builtin", params)
949
+
950
+ # RD → MD → RD → parse
951
+ rrd = File.read(rrd_path)
952
+ md = BitClust::RRDToMarkdown.convert(rrd)
953
+ rrd2 = BitClust::MarkdownToRRD.convert(md)
954
+
955
+ require 'tempfile'
956
+ Tempfile.create(['comparable', '.rrd']) do |f|
957
+ f.write(rrd2)
958
+ f.flush
959
+ db2 = BitClust::MethodDatabase.dummy(params)
960
+ lib2 = BitClust::RRDParser.new(db2).parse_file(f.path, "_builtin", params)
961
+
962
+ # 同じクラス/メソッド構造であることを検証
963
+ orig_methods = lib1.classes.first.entries.map(&:names)
964
+ roundtrip_methods = lib2.classes.first.entries.map(&:names)
965
+ assert_equal orig_methods, roundtrip_methods
966
+ end
967
+ end
968
+ end