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,1276 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bitclust/mdcompiler'
4
+ require 'bitclust/rdcompiler'
5
+ require 'bitclust/rrd_to_markdown'
6
+ require 'bitclust/database'
7
+ require 'bitclust/methoddatabase'
8
+ require 'bitclust/screen'
9
+ require 'test/unit'
10
+ require 'test/unit/rr'
11
+ require 'stringio'
12
+
13
+ # MDCompiler: Markdown ソース → HTML のネイティブコンパイラ(フェーズ3 M1)。
14
+ # M1 の不変条件: 変換器が生成する md に対して、対応する rd を RDCompiler に
15
+ # かけた場合と同一の HTML を出力する(等価モード)。
16
+ # GFM 拡張(テーブル・コードスパンの <code> 化等)は M2 で解禁する。
17
+ class TestMDCompiler < Test::Unit::TestCase
18
+ def setup
19
+ @dummy = 'dummy'
20
+ @u = BitClust::URLMapper.new(Hash.new { @dummy })
21
+ @db = BitClust::MethodDatabase.dummy("version" => "2.0.0")
22
+ @md = BitClust::MDCompiler.new(@u, 1, { :database => @db })
23
+ @rd = BitClust::RDCompiler.new(@u, 1, { :database => @db })
24
+ end
25
+
26
+ def compile_method(compiler, src)
27
+ method_entry = Object.new
28
+ mock(method_entry).source { src }
29
+ mock(method_entry).index_id.any_times { "dummy" }
30
+ mock(method_entry).defined?.any_times { true }
31
+ mock(method_entry).id.any_times { "String/i.index._builtin" }
32
+ mock(method_entry).names.any_times { [] }
33
+ mock(method_entry).since_map.any_times { {} }
34
+ mock(method_entry).until_map.any_times { {} }
35
+ compiler.compile_method(method_entry)
36
+ end
37
+
38
+ # rd ソースを両経路(rd 直接 / md 変換後)でコンパイルして等価を確認する
39
+ def assert_equivalent_method(rd_src)
40
+ md_src = BitClust::RRDToMarkdown.convert(rd_src)
41
+ assert_equal compile_method(@rd, rd_src), compile_method(@md, md_src),
42
+ "md source:\n#{md_src}"
43
+ end
44
+
45
+ def assert_equivalent_doc(rd_src)
46
+ md_src = BitClust::RRDToMarkdown.convert(rd_src)
47
+ assert_equal @rd.compile(rd_src), @md.compile(md_src), "md source:\n#{md_src}"
48
+ end
49
+
50
+ def capture_stderr
51
+ orig = $stderr
52
+ $stderr = StringIO.new
53
+ yield
54
+ $stderr.string
55
+ ensure
56
+ $stderr = orig
57
+ end
58
+
59
+ # ---- メソッドエントリ ----
60
+
61
+ def test_method_signature_and_paragraph
62
+ assert_equivalent_method <<~RD
63
+ --- index(val) -> Integer
64
+
65
+ 説明文。
66
+ 2行目。
67
+ RD
68
+ end
69
+
70
+ def test_multiple_signatures
71
+ assert_equivalent_method <<~RD
72
+ --- index(val) -> Integer
73
+ --- index {|item| ... } -> Integer
74
+
75
+ 説明。
76
+ RD
77
+ end
78
+
79
+ def test_param_return_raise
80
+ assert_equivalent_method <<~RD
81
+ --- index(val) -> Integer
82
+
83
+ 説明。
84
+
85
+ @param val 探す値。
86
+ 継続行。
87
+ @return 位置。
88
+ @raise TypeError 型が合わないとき。
89
+ RD
90
+ end
91
+
92
+ def test_see
93
+ assert_equivalent_method <<~RD
94
+ --- index(val) -> Integer
95
+
96
+ 説明。
97
+
98
+ @see [[m:Array#rindex]], [[m:Array#find_index]]
99
+ RD
100
+ end
101
+
102
+ def test_inline_references
103
+ assert_equivalent_method <<~RD
104
+ --- index(val) -> Integer
105
+
106
+ [[m:Array#rindex]] と [[c:String]] を参照。
107
+ [[m:Hash#[] ]] のような括弧メソッドも。
108
+ RD
109
+ end
110
+
111
+ def test_inline_ref_with_hyphenated_fragment
112
+ # [ref:d:page#frag] のフラグメントはハイフンを含められる
113
+ # (doctree/manual の glossary.md 用語アンカー等、rd 直接/md 変換後で等価)
114
+ assert_equivalent_method <<~RD
115
+ --- index(val) -> Integer
116
+
117
+ [[ref:d:hoge/bar#thread-safe]] を参照。
118
+ RD
119
+ end
120
+
121
+ def test_code_block_from_emlist
122
+ assert_equivalent_method <<~RD
123
+ --- index(val) -> Integer
124
+
125
+ 例:
126
+
127
+ //emlist[例][ruby]{
128
+ [1, 2].index(2) # => 1
129
+ //}
130
+ RD
131
+ end
132
+
133
+ def test_plain_emlist_block
134
+ assert_equivalent_method <<~RD
135
+ --- index(val) -> Integer
136
+
137
+ //emlist{
138
+ plain text art
139
+ //}
140
+ RD
141
+ end
142
+
143
+ def test_indented_code_block
144
+ assert_equivalent_method <<~RD
145
+ --- index(val) -> Integer
146
+
147
+ 例:
148
+
149
+ [1, 2].index(2)
150
+ # => 1
151
+
152
+ 続き。
153
+ RD
154
+ end
155
+
156
+ def test_indented_code_with_tabs
157
+ # Kernel#mkmf 型: タブは元のカラム位置で展開される(detab はデデント前)
158
+ assert_equivalent_method "--- mkmf -> ()\n\n説明。\n\n -d ARGS\trun dir_config\n -h ARGS\trun have_header\n"
159
+ end
160
+
161
+ def test_item_list
162
+ assert_equivalent_method <<~RD
163
+ --- index(val) -> Integer
164
+
165
+ 説明:
166
+
167
+ * 項目1
168
+ * 項目2
169
+ 継続行。
170
+ RD
171
+ end
172
+
173
+ def test_dlist
174
+ assert_equivalent_method <<~RD
175
+ --- index(val) -> Integer
176
+
177
+ : term1
178
+ 説明1。
179
+ : term2
180
+ 説明2。
181
+ RD
182
+ end
183
+
184
+ def test_dlist_term_anchor
185
+ # 用語の後ろの {#id} は dt のアンカー id になる(用語集の各用語リンク用, rurema/doctree#2634)
186
+ html = @md.compile("- **アリティー**: {#arity}\n- **`arity`**:\n 仮引数の数。\n")
187
+ assert_include(html, '<dt id="arity">アリティー</dt>')
188
+ # {#id} が無い項目には id は付かない
189
+ assert_not_include(html, '<dt id="arity">arity')
190
+ # 通常の dlist は従来どおり id 無し
191
+ plain = @md.compile("- **foo**:\n bar\n")
192
+ assert_include(plain, '<dt>foo</dt>')
193
+ assert_not_include(plain, '<dt id')
194
+ end
195
+
196
+ def test_entry_heading
197
+ assert_equivalent_method <<~RD
198
+ --- index(val) -> Integer
199
+
200
+ 説明。
201
+
202
+ ===[a:anchor] 深掘り
203
+
204
+ 本文。
205
+ RD
206
+ end
207
+
208
+ def test_entry_heading_with_hyphenated_anchor
209
+ # ハイフン入りアンカー(doctree/manual の glossary.md 用語アンカー等)でも
210
+ # rd 直接コンパイルと md 変換後コンパイルが等価であること
211
+ assert_equivalent_method <<~RD
212
+ --- index(val) -> Integer
213
+
214
+ 説明。
215
+
216
+ ===[a:thread-safe] スレッドセーフ
217
+
218
+ 本文。
219
+ RD
220
+ end
221
+
222
+ def test_const_signature
223
+ assert_equivalent_method <<~RD
224
+ --- INDEX_VERSION -> String
225
+
226
+ 定数の説明。
227
+ RD
228
+ end
229
+
230
+ def test_param_description_with_example_block
231
+ # CGI::HtmlExtension#popup_menu 型: 空白のみ行を挟む例示ブロックも @param の説明
232
+ assert_equivalent_method <<~RD
233
+ --- m(v) -> String
234
+
235
+ 説明。
236
+
237
+ @param v 説明。
238
+
239
+ 例:
240
+ code_like_text
241
+ RD
242
+ end
243
+
244
+ def test_param_description_with_emlist
245
+ # Array#pack 型: @param の説明に //emlist が続く(rd では dd の一部)
246
+ assert_equivalent_method <<~RD
247
+ --- m(v) -> String
248
+
249
+ 説明。
250
+
251
+ @param v 値。
252
+
253
+ //emlist[例][ruby]{
254
+ p 1
255
+ //}
256
+
257
+ @return 結果。
258
+ RD
259
+ end
260
+
261
+ def test_adjacent_fences_with_blank_line_merge
262
+ # IO#sysseek 型: ゲート分断されたインデントブロックは版解決後に
263
+ # 空白のみ行を挟む隣接フェンスとして現れる。rd の pre は空白行を
264
+ # 跨ぐため、間の空白行ごと1つの <pre> にマージする
265
+ rd_src = "--- m(v) -> String\n\n説明。\n\n" \
266
+ " line1\n # => result\n \n\n line2\n"
267
+ md_src = "### def m(v) -> String\n\n説明。\n\n" \
268
+ "`````\nline1\n`````\n`````\n# => result\n`````\n \n\n`````\nline2\n`````\n"
269
+ assert_equal compile_method(@rd, rd_src), compile_method(@md, md_src),
270
+ "md source:\n#{md_src}"
271
+ end
272
+
273
+ def test_leftover_emlist_in_doc
274
+ # news/2_5_0 型: リスト脈絡の #@samplecode は md に生のまま残り、
275
+ # 前処理で //emlist になる。RDCompiler と同じく独立ブロックとして描画する
276
+ rd_src = " * 項目 [[m:Coverage.start]]\n//emlist[][ruby]{\nCoverage.start\n//}\n * 次の項目\n"
277
+ md_src = " - 項目 [m:Coverage.start]\n//emlist[][ruby]{\nCoverage.start\n//}\n - 次の項目\n"
278
+ assert_equal @rd.compile(rd_src), @md.compile(md_src), "md source:\n#{md_src}"
279
+ end
280
+
281
+ def test_leftover_emlist_in_method_entry
282
+ rd_src = "--- m(v) -> String\n\n説明。\n//emlist[][ruby]{\np 1\n//}\n"
283
+ md_src = "### def m(v) -> String\n\n説明。\n//emlist[][ruby]{\np 1\n//}\n"
284
+ assert_equal compile_method(@rd, rd_src), compile_method(@md, md_src),
285
+ "md source:\n#{md_src}"
286
+ end
287
+
288
+ def test_doc_see_renders_see_also
289
+ # findings#1: doc/lib ページの @see も SEE_ALSO として解釈する
290
+ # (従来は RDCompiler の library_file に分岐がなく段落テキストだった)
291
+ rd_src = "= 見出し\n\n説明。\n\n@see [[m:Array#each]]\n"
292
+ md_src = BitClust::RRDToMarkdown.convert(rd_src)
293
+ rd_html = @rd.compile(rd_src)
294
+ assert_match(/\[SEE_ALSO\]/, rd_html)
295
+ assert_equal rd_html, @md.compile(md_src), "md source:\n#{md_src}"
296
+ end
297
+
298
+ def test_entry_paragraph_with_midline_see_not_swallowed
299
+ # findings#3: 行頭アンカー無しの /@see/ dispatch が本文行を see() に
300
+ # 吸う潜在バグ。段落の先頭行に「@see」を含む文があっても段落のまま
301
+ rd_src = "--- m(v) -> String\n\n本文中で @see を説明する行。\n"
302
+ html = compile_method(@rd, rd_src)
303
+ assert_not_match(/SEE_ALSO/, html)
304
+ assert_match(/本文中で @see を説明する行。/, html)
305
+ end
306
+
307
+ def test_undef_renders_dedicated_message
308
+ # {: undef} 属性行は専用の説明文を描画する(statichtml は undefined を
309
+ # skip するため露出は server 等の動的経路のみ)
310
+ rd_src = "--- <(other) -> bool\n{: undef}\n"
311
+ md_src = BitClust::RRDToMarkdown.convert(rd_src)
312
+ rd_html = compile_method(@rd, rd_src)
313
+ assert_not_match(/UNKNOWN_META_INFO/, rd_html)
314
+ assert_match(/このメソッドは定義されていません/, rd_html)
315
+ assert_equal rd_html, compile_method(@md, md_src), "md source:\n#{md_src}"
316
+ end
317
+
318
+ def test_signature_without_space
319
+ # OpenSSL::X509::Extension 型: 「---name」(スペース無し)を RDCompiler は受理する
320
+ assert_equivalent_method <<~RD
321
+ ---critical=(bool)
322
+ 重要度を設定します。
323
+
324
+ @param bool 真偽値
325
+ RD
326
+ end
327
+
328
+ def test_raise_without_description_trailing_space
329
+ # Gem::RemoteFetcher#download 型: 「@raise Ex 」(説明なし・末尾スペース)
330
+ # RDCompiler は dd 内に空白テキスト行を出す
331
+ assert_equivalent_method "--- m(v) -> String\n\n説明。\n\n@raise SomeError \n"
332
+ end
333
+
334
+ def test_indented_code_across_double_blank
335
+ # Process#exec 型: 空行2つを挟むインデントコードは1つの <pre> に融合する
336
+ assert_equivalent_method <<~RD
337
+ --- m(v) -> String
338
+
339
+ 例:
340
+
341
+ exec "echo *"
342
+ # never get here
343
+
344
+
345
+ exec "echo", "*"
346
+ # never get here
347
+
348
+ @param v 値。
349
+ RD
350
+ end
351
+
352
+ def test_undef_metadata
353
+ # Complex#< 型: {: undef} 属性行。両経路とも「定義されていません」を描画
354
+ assert_equivalent_method <<~RD
355
+ --- <(other) -> bool
356
+ {: undef}
357
+ RD
358
+ end
359
+
360
+ def test_nomethod_metadata
361
+ # @nomethod(説明のための未定義メソッド)はマーカーなので本文には描画しない
362
+ md_src = BitClust::RRDToMarkdown.convert(<<~RD)
363
+ --- to_a -> Array
364
+ {: nomethod}
365
+
366
+ 説明のためここに記載しています。
367
+ RD
368
+ html = compile_method(@md, md_src)
369
+ assert_not_include(html, 'UNKNOWN_META_INFO')
370
+ assert_not_include(html, '{:')
371
+ assert_include(html, '説明のためここに記載しています。')
372
+ end
373
+
374
+ def test_nomethod_metadata_equivalence
375
+ assert_equivalent_method <<~RD
376
+ --- to_a -> Array
377
+ {: nomethod}
378
+
379
+ 説明のためここに記載しています。
380
+ RD
381
+ end
382
+
383
+ def test_unknown_metadata_after_param
384
+ # @param に続く未知メタデータは同じ <dl> 内に UNKNOWN_META_INFO で入る
385
+ assert_equivalent_method <<~RD
386
+ --- m(v) -> bool
387
+
388
+ 説明。
389
+
390
+ @param v 値。
391
+ @undef
392
+ RD
393
+ end
394
+
395
+ def test_dlist_description_with_interleaved_emlist
396
+ # String#% 型: dd は「インデント段落 + emlist」を交互に何個でも受ける。
397
+ # 1つ目の継続(プレフィックスを…)は dt の直後(空行なし)なので
398
+ # lazy continuation としてインデント1でも dd に入るが、2つ目
399
+ # (浮動小数点数…)は emlist の後の空行を挟むため、CommonMark 準拠の
400
+ # ためマーカー幅(2)と同じインデントにしている(#3232 対応)
401
+ assert_equivalent_method <<~RD
402
+ --- m(v) -> String
403
+
404
+ : #
405
+ プレフィックスを付けます。
406
+
407
+ //emlist[][ruby]{
408
+ p 1
409
+ //}
410
+
411
+ 浮動小数点数に対しては必ず付けます。
412
+
413
+ //emlist[][ruby]{
414
+ p 2
415
+ //}
416
+
417
+ 続き。
418
+ RD
419
+ end
420
+
421
+ def test_dlist_with_list_like_text_in_description
422
+ # news/1.8.0 型: dd 内のリスト風テキストは RDCompiler ではただのテキスト
423
+ assert_equivalent_doc <<~RD
424
+ = タイトル
425
+
426
+ : term
427
+ 説明。
428
+
429
+ * 項目風1
430
+ * 項目風2
431
+ RD
432
+ end
433
+
434
+ def test_colon_line_as_paragraph_continuation
435
+ # news/1.8.5 型: テキスト行直後の「: 」行は段落の継続
436
+ assert_equivalent_doc <<~RD
437
+ = タイトル
438
+
439
+ 以下が追加されました。
440
+ : TCPServer#accept_nonblock [new]
441
+
442
+ 本文。
443
+ RD
444
+ end
445
+
446
+ def test_list_item_with_shallow_continuation
447
+ # news/2_6_0 型: 項目より浅い折り返しも項目の継続
448
+ assert_equivalent_doc <<~RD
449
+ = タイトル
450
+
451
+ * 項目の一行目が長くて
452
+ 折り返した継続行。
453
+
454
+ 本文。
455
+ RD
456
+ end
457
+
458
+ def test_h5_heading
459
+ # spec/safelevel 型: ===== は h3(hlevel 1 + (5-3))として描画される
460
+ assert_equivalent_doc <<~RD
461
+ = タイトル
462
+
463
+ ==== レベル 0
464
+
465
+ ===== 汚染されるオブジェクト
466
+
467
+ 本文。
468
+ RD
469
+ end
470
+
471
+ def test_olist_item_continuation
472
+ # spec/terminate 型: (N) 項目の折り返し行は項目の継続
473
+ assert_equivalent_doc <<~RD
474
+ = タイトル
475
+
476
+ (1) すべてのスレッドを kill
477
+ (2) ハンドラが登録されていればそれを実
478
+ 行する。
479
+ (3) 後始末。
480
+
481
+ 本文。
482
+ RD
483
+ end
484
+
485
+ def test_see_in_doc_is_plain_text
486
+ # pack_template 型: doc/lib ページの @see は RDCompiler では段落テキスト
487
+ assert_equivalent_doc <<~RD
488
+ = タイトル
489
+
490
+ 本文。
491
+
492
+ @see [[c:String]]
493
+ RD
494
+ end
495
+
496
+ def test_dlist_colon_term_without_space
497
+ # spec/operator 型: dlist 継続の「:term」(スペース無し)も dt
498
+ assert_equivalent_doc <<~RD
499
+ = タイトル
500
+
501
+ : 再定義できる演算子
502
+ いろいろ。
503
+
504
+ :再定義できない演算子
505
+ これらは再定義できません。
506
+
507
+ 本文。
508
+ RD
509
+ end
510
+
511
+ def test_colon_no_space_in_paragraph_stays_text
512
+ # openssl/ASN1 型: 段落継続の「:SYMBOL」行は dlist 化しない
513
+ assert_equivalent_doc <<~RD
514
+ = タイトル
515
+
516
+ タグクラスを返します。
517
+ :IMPLICIT、:EXPLICIT、nil のいずれかを返します。
518
+
519
+ 本文。
520
+ RD
521
+ end
522
+
523
+ def test_discrete_numbered_text
524
+ # lib:logger 型: 離散した「N. テキスト」は RD では段落テキスト
525
+ # (md では **N.** 太字。M1 では元のテキストに戻して描画する)
526
+ assert_equivalent_doc <<~RD
527
+ = タイトル
528
+
529
+ ログの出力先について。
530
+
531
+ 1. STDERR/STDOUTに出力するように指定
532
+
533
+ 本文が続きます。
534
+
535
+ 2. ファイルに出力するように指定
536
+ RD
537
+ end
538
+
539
+ # ---- M2: GFM モード(:gfm オプション) ----
540
+ # M1 等価モード(既定)と違い、RDCompiler と同一 HTML ではなく
541
+ # GFM の表現(<code>/<strong>/<table>)を描画する
542
+
543
+ def gfm_compiler
544
+ BitClust::MDCompiler.new(@u, 1, { :database => @db, :gfm => true })
545
+ end
546
+
547
+ def test_gfm_code_span_in_paragraph
548
+ html = gfm_compiler.compile("# タイトル\n\nこの `code` はスパンです。\n")
549
+ assert_include html, "この <code>code</code> はスパンです。"
550
+ end
551
+
552
+ def test_gfm_escaped_backtick_stays_literal
553
+ html = gfm_compiler.compile("# タイトル\n\n\\`--version' と \\`-' の話。\n")
554
+ assert_include html, "`--version' と `-' の話。"
555
+ assert_not_include html, "<code>"
556
+ end
557
+
558
+ def test_gfm_auto_span_becomes_code
559
+ html = gfm_compiler.compile("# タイトル\n\n`__FILE__` を返します。\n")
560
+ assert_include html, "<code>__FILE__</code> を返します。"
561
+ end
562
+
563
+ def test_gfm_code_span_escapes_html
564
+ html = gfm_compiler.compile("# タイトル\n\n`a<b>` です。\n")
565
+ assert_include html, "<code>a&lt;b&gt;</code> です。"
566
+ end
567
+
568
+ # ---- CommonMark 6.1: N 連バッククォートのコードスパン(doctree#3232) ----
569
+
570
+ def test_gfm_double_backtick_span_contains_single_backtick
571
+ # 開始と同じ長さ(2連)のバッククォート列で閉じ、前後の空白を1個ずつ剥ぐ
572
+ html = gfm_compiler.compile("# T\n\n`` foo ` bar `` です。\n")
573
+ assert_include html, "<code>foo ` bar</code> です。"
574
+ end
575
+
576
+ def test_gfm_triple_backtick_span
577
+ # 行頭 ``` はブロックレベルのフェンスと解釈されるため、文中に置く
578
+ html = gfm_compiler.compile("# T\n\n見よ ``` code ``` です。\n")
579
+ assert_include html, "<code>code</code> です。"
580
+ end
581
+
582
+ def test_gfm_unbalanced_backtick_run_stays_literal
583
+ # 閉じる相手のいない開始列はコードスパンにせずリテラルのまま
584
+ html = gfm_compiler.compile("# T\n\n``foo` です。\n")
585
+ assert_include html, "``foo` です。"
586
+ assert_not_include html, "<code>"
587
+ end
588
+
589
+ def test_gfm_escaped_backtick_inside_span_keeps_backslash
590
+ # コードスパン内ではバックスラッシュエスケープが無効(CommonMark 6.1)。
591
+ # \` はバックスラッシュごとリテラルに残る(GitHub の実描画と同一)
592
+ html = gfm_compiler.compile("# T\n\n`` \\` `` は変換されません。\n")
593
+ assert_include html, "<code>\\`</code> は変換されません。"
594
+ end
595
+
596
+ def test_gfm_bare_backtick_in_bracket_ref_still_resolves
597
+ # 既存の [m:$`](素のバッククォート)の後方互換
598
+ html = gfm_compiler.compile("# T\n\n[m:$`] を参照。\n")
599
+ assert_include html, ">$`</a>"
600
+ assert_not_include html, "[[m:$"
601
+ end
602
+
603
+ def test_gfm_escaped_backtick_in_bracket_ref_resolves
604
+ # znz さん提示の書き方(doctree#3232 レビュー): [m:$\`] は
605
+ # [m:$`](素のバッククォート)と同じ参照へ解決される
606
+ escaped = gfm_compiler.compile("# T\n\n[m:$\\`] を参照。\n")
607
+ bare = gfm_compiler.compile("# T\n\n[m:$`] を参照。\n")
608
+ assert_equal bare, escaped
609
+ assert_include escaped, ">$`</a>"
610
+ assert_not_include escaped, "[[m:$"
611
+ assert_not_include escaped, "\\`"
612
+ end
613
+
614
+ def test_gfm_znz_gsub_paragraph_golden
615
+ # doctree PR #3232 レビューで znz さんが提示した段落そのもの
616
+ # (String#gsub の説明)。GitHub 上の実描画(gh api -X POST /markdown で
617
+ # 確認済み)と同等の構造 — \`・\'・\+ がコードスパンに、
618
+ # 3つの参照がすべて解決されること — をまとめて確認する
619
+ md = <<~MD
620
+ # T
621
+
622
+ 置換文字列内では `` \\` ``、`\\'`、`\\+` も使えます。
623
+ これらは [m:$\\`]、[m:$']、[m:$+] に対応します。
624
+ MD
625
+ html = gfm_compiler.compile(md)
626
+ assert_include html, "<code>\\`</code>"
627
+ assert_include html, "<code>\\'</code>"
628
+ assert_include html, "<code>\\+</code>"
629
+ assert_include html, ">$`</a>"
630
+ assert_include html, ">$'</a>"
631
+ assert_include html, ">$+</a>"
632
+ assert_not_include html, "[[m:$"
633
+ assert_not_include html, "[m:$"
634
+ end
635
+
636
+ def test_gfm_ref_and_span_coexist
637
+ html = gfm_compiler.compile("# タイトル\n\n[c:String] と `x` を参照。\n")
638
+ assert_include html, "</a> と <code>x</code> を参照。"
639
+ end
640
+
641
+ def test_gfm_strong_number
642
+ html = gfm_compiler.compile("# タイトル\n\n**1.** 最初の説明。\n")
643
+ assert_include html, "<strong>1.</strong> 最初の説明。"
644
+ end
645
+
646
+ def test_gfm_inline_strong
647
+ html = gfm_compiler.compile("# タイトル\n\nこの **重要** な点。\n")
648
+ assert_include html, "この <strong>重要</strong> な点。"
649
+ end
650
+
651
+ def test_gfm_inline_strong_no_space_boundary
652
+ # 和文は空白なしで隣接する
653
+ html = gfm_compiler.compile("# タイトル\n\nこれは**とても重要**です。\n")
654
+ assert_include html, "これは<strong>とても重要</strong>です。"
655
+ end
656
+
657
+ def test_gfm_inline_strong_multiple
658
+ html = gfm_compiler.compile("# タイトル\n\n**a** と **b** の話。\n")
659
+ assert_include html, "<strong>a</strong> と <strong>b</strong> の話。"
660
+ end
661
+
662
+ def test_gfm_inline_strong_wraps_ref
663
+ html = gfm_compiler.compile("# タイトル\n\n**[c:String]** を参照。\n")
664
+ assert_include html, "<strong><a href="
665
+ assert_include html, "</a></strong> を参照。"
666
+ end
667
+
668
+ def test_gfm_inline_strong_wraps_code_span
669
+ html = gfm_compiler.compile("# タイトル\n\n**`freeze` される**ので注意。\n")
670
+ assert_include html, "<strong><code>freeze</code> される</strong>ので注意。"
671
+ end
672
+
673
+ def test_gfm_inline_strong_in_code_span_stays_literal
674
+ html = gfm_compiler.compile("# タイトル\n\n`**not strong**` と書く。\n")
675
+ assert_include html, "<code>**not strong**</code> と書く。"
676
+ assert_not_include html, "<strong>"
677
+ end
678
+
679
+ def test_gfm_inline_strong_exponent_stays_literal
680
+ # 単語内の ** は冪乗演算子とみなして適用しない
681
+ html = gfm_compiler.compile("# タイトル\n\n値は 2**32 と 2**64 です。\n")
682
+ assert_not_include html, "<strong>"
683
+ assert_include html, "2**32 と 2**64"
684
+ end
685
+
686
+ def test_gfm_inline_strong_space_boundary_stays_literal
687
+ html = gfm_compiler.compile("# タイトル\n\na ** b ** c の形。\n")
688
+ assert_not_include html, "<strong>"
689
+ end
690
+
691
+ def test_gfm_inline_strong_spans_soft_break
692
+ # 段落内の改行は行結合で除去されるので、GFM と同じく soft break をまたげる
693
+ html = gfm_compiler.compile("# タイトル\n\n**改行を\nまたぐ**形。\n")
694
+ assert_include html, "<strong>改行をまたぐ</strong>形。"
695
+ end
696
+
697
+ def test_gfm_inline_strong_triple_asterisk_stays_literal
698
+ html = gfm_compiler.compile("# タイトル\n\n***em strong*** は対象外。\n")
699
+ assert_not_include html, "<strong>"
700
+ end
701
+
702
+ def test_gfm_param_name_code
703
+ md = "### def m(v) -> String\n\n説明。\n\n- **param** `v` -- 値。\n"
704
+ html = compile_method(gfm_compiler, md)
705
+ assert_include html, "[PARAM] <code>v</code>:"
706
+ end
707
+
708
+ def test_gfm_raise_name_code
709
+ md = "### def m(v) -> String\n\n説明。\n\n- **raise** `TypeError` -- 型エラー。\n"
710
+ html = compile_method(gfm_compiler, md)
711
+ assert_include html, "[EXCEPTION] <code>TypeError</code>:"
712
+ end
713
+
714
+ def test_gfm_dlist_code_term
715
+ html = gfm_compiler.compile("# タイトル\n\n- **`type`**: Content-Type ヘッダ。\n")
716
+ assert_include html, "<dt><code>type</code></dt>"
717
+ end
718
+
719
+ def test_gfm_dlist_ref_term_resolves_inside_code
720
+ # spec/eval 型: ref のコード term は <code> 内でリンク解決する
721
+ html = gfm_compiler.compile("# T\n\n- **`[c:String]`**: 文字列クラス。\n")
722
+ assert_include html, "<dt><code><a href="
723
+ assert_include html, "</a></code></dt>"
724
+ end
725
+
726
+ def test_gfm_dlist_term_inner_stripped
727
+ # news/1.8.2 型: スパン内の末尾スペースは dt に含めない(rd の strip と同じ)
728
+ html = gfm_compiler.compile("# T\n\n- **`CSV.open, and generate `**: 説明。\n")
729
+ assert_include html, "<dt><code>CSV.open, and generate</code></dt>"
730
+ end
731
+
732
+ def test_gfm_dlist_plain_term_stays_plain
733
+ html = gfm_compiler.compile("# タイトル\n\n- **用語**: 説明文。\n")
734
+ assert_include html, "<dt>用語</dt>"
735
+ end
736
+
737
+ def test_gfm_table_basic
738
+ md = <<~MD
739
+ # タイトル
740
+
741
+ | 文字列 | ステータス |
742
+ |--------|-----------|
743
+ | `"OK"` | 200 |
744
+ MD
745
+ html = gfm_compiler.compile(md)
746
+ assert_include html, "<table>"
747
+ assert_include html, "<th>文字列</th>"
748
+ assert_include html, "<td><code>&quot;OK&quot;</code></td>"
749
+ assert_include html, "<td>200</td>"
750
+ end
751
+
752
+ def test_gfm_table_alignment
753
+ md = "# T\n\n| a | b | c |\n|:--|:-:|--:|\n| 1 | 2 | 3 |\n"
754
+ html = gfm_compiler.compile(md)
755
+ assert_include html, '<th align="left">a</th>'
756
+ assert_include html, '<th align="center">b</th>'
757
+ assert_include html, '<th align="right">c</th>'
758
+ assert_include html, '<td align="right">3</td>'
759
+ end
760
+
761
+ def test_gfm_pipe_line_without_delimiter_is_paragraph
762
+ # FalseClass 型: | 演算子の散文はテーブルにしない(区切り行必須)
763
+ md = "# T\n\n| は再定義可能な演算子です。通常は false | other の形で使われます。\n"
764
+ html = gfm_compiler.compile(md)
765
+ assert_not_include html, "<table>"
766
+ assert_include html, "| は再定義可能な演算子です。"
767
+ end
768
+
769
+ def test_gfm_off_by_default
770
+ html = @md.compile("# タイトル\n\nこの `code' はどうか。\n")
771
+ assert_not_include html, "<code>code"
772
+ end
773
+
774
+ # ---- M2: Markdown リンク(MARKUP_SPEC §7.4/§7.5、news/1_9_0 型) ----
775
+
776
+ def test_gfm_autolink
777
+ html = gfm_compiler.compile("# T\n\n<https://example.com/a#b> を参照。\n")
778
+ assert_include html,
779
+ '<a class="external" href="https://example.com/a#b">https://example.com/a#b</a> を参照。'
780
+ end
781
+
782
+ def test_gfm_autolink_in_dlist_description
783
+ md = "# T\n\n- **`__callee__`**:\n\n <https://example.com/x>\n"
784
+ html = gfm_compiler.compile(md)
785
+ assert_include html, '<a class="external" href="https://example.com/x">https://example.com/x</a>'
786
+ end
787
+
788
+ def test_gfm_autolink_requires_url_scheme
789
+ html = gfm_compiler.compile("# T\n\na <b> c です。\n")
790
+ assert_include html, "a &lt;b&gt; c です。"
791
+ assert_not_include html, "<a "
792
+ end
793
+
794
+ def test_gfm_inline_link
795
+ html = gfm_compiler.compile("# T\n\n[例示ドメイン](https://example.com)のサポート\n")
796
+ assert_include html, '<a class="external" href="https://example.com">例示ドメイン</a>のサポート'
797
+ end
798
+
799
+ def test_gfm_inline_link_with_bracketed_text
800
+ # news/1_9_0 型: [[ruby-cvs:16833]](アーカイブURL) — 表示テキストに角括弧
801
+ html = gfm_compiler.compile("# T\n\n[[ruby-cvs:16833]](https://example.com/16833)\n")
802
+ assert_include html, '<a class="external" href="https://example.com/16833">[ruby-cvs:16833]</a>'
803
+ end
804
+
805
+ def test_gfm_inline_link_text_with_parens_and_url_query
806
+ md = "# T\n\n[patch: activity in 1.9 (2006-06)](https://example.com/hiki.rb?Changes+in+Ruby)\n"
807
+ html = gfm_compiler.compile(md)
808
+ assert_include html,
809
+ '<a class="external" href="https://example.com/hiki.rb?Changes+in+Ruby">patch: activity in 1.9 (2006-06)</a>'
810
+ end
811
+
812
+ def test_gfm_fragment_link
813
+ html = gfm_compiler.compile("# T\n\n[破壊的な変更](#mutable) を参照。\n")
814
+ assert_include html, '<a href="#mutable">破壊的な変更</a> を参照。'
815
+ end
816
+
817
+ def test_gfm_non_url_destination_stays_text
818
+ # 参照直後の括弧書きをリンクと誤認しない(宛先は URL/#フラグメントのみ)
819
+ html = gfm_compiler.compile("# T\n\n[c:String](文字列)を参照。\n")
820
+ assert_include html, "</a>(文字列)を参照。"
821
+ end
822
+
823
+ def test_gfm_link_inside_code_span_stays_code
824
+ html = gfm_compiler.compile("# T\n\n`[x](https://example.com)` はコード。\n")
825
+ assert_include html, "<code>[x](https://example.com)</code> はコード。"
826
+ end
827
+
828
+ def test_gfm_link_escapes_html_in_text_and_url
829
+ html = gfm_compiler.compile("# T\n\n[a<b>](https://example.com/?q=<x>)\n")
830
+ assert_include html, '<a class="external" href="https://example.com/?q=&lt;x&gt;">a&lt;b&gt;</a>'
831
+ end
832
+
833
+ # ---- M2: インデントされたコードフェンス(リスト・dlist 内、news/1_9_0 型) ----
834
+
835
+ def test_gfm_indented_fence_in_dlist_description
836
+ md = <<~MD
837
+ # T
838
+
839
+ - **`Dir.glob`**:
840
+
841
+ 説明文です。
842
+
843
+ ```ruby
844
+ p Dir.glob(["f*","b*"]) # => ["foo", "bar"]
845
+ ```
846
+
847
+ 続きの説明。
848
+ MD
849
+ html = gfm_compiler.compile(md)
850
+ assert_include html, '<pre class="highlight ruby">'
851
+ assert_include html, 'Dir.glob'
852
+ assert_not_include html, '```'
853
+ # 内容はフェンス行のインデント分がデデントされる
854
+ assert_include html, "<span class=\"nb\">p</span>"
855
+ assert_include html, "<p>\n続きの説明。\n</p>"
856
+ end
857
+
858
+ def test_gfm_indented_fence_without_lang
859
+ md = "# T\n\n- **`proc`**:\n\n 説明。\n\n ```text\n x = {|a| p a}\n ```\n"
860
+ html = gfm_compiler.compile(md)
861
+ assert_include html, "x = {|a| p a}"
862
+ assert_not_include html, '```'
863
+ end
864
+
865
+ def test_gfm_indented_fence_in_list_item
866
+ md = "# T\n\n- 項目\n ```ruby\n p 1\n ```\n- 次の項目\n"
867
+ html = gfm_compiler.compile(md)
868
+ assert_include html, '<pre class="highlight ruby">'
869
+ assert_include html, "次の項目"
870
+ assert_not_include html, '```'
871
+ end
872
+
873
+ def test_gfm_indented_fence_at_top_level
874
+ # リスト項目と空行で切り離されたフェンス(行駆動モデルではトップレベル扱い)
875
+ md = "# T\n\n- 項目\n\n ```ruby\n p 1\n ```\n\n本文。\n"
876
+ html = gfm_compiler.compile(md)
877
+ assert_include html, '<pre class="highlight ruby">'
878
+ assert_include html, "本文。"
879
+ assert_not_include html, '```'
880
+ end
881
+
882
+ def test_gfm_indented_fence_in_method_entry
883
+ md = "### def m(v) -> String\n\n説明。\n\n- **`opt`**:\n\n ```ruby\n p 1\n ```\n"
884
+ html = compile_method(gfm_compiler, md)
885
+ assert_include html, '<pre class="highlight ruby">'
886
+ assert_not_include html, '```'
887
+ end
888
+
889
+ def test_gfm_indented_fence_in_param_continuation
890
+ md = "### def m(v) -> String\n\n説明。\n\n- **param** `v` -- 値。\n\n ```ruby\n p 1\n ```\n"
891
+ html = compile_method(gfm_compiler, md)
892
+ assert_include html, '<pre class="highlight ruby">'
893
+ assert_not_include html, '```'
894
+ end
895
+
896
+ def test_gfm_stray_indented_line_does_not_hang
897
+ # リストと空行で切り離された残余のインデント行はどのブロックにも
898
+ # 該当しない。段落として消費する(従来はディスパッチが進まずハング)
899
+ md = "# T\n\n- 項目\n\n 切り離された継続行。\n\n本文。\n"
900
+ html = gfm_compiler.compile(md)
901
+ assert_include html, "切り離された継続行。"
902
+ assert_include html, "本文。"
903
+ end
904
+
905
+ def test_gfm_stray_indented_line_in_method_entry_does_not_hang
906
+ md = "### def m(v) -> String\n\n- 項目\n\n 切り離された継続行。\n"
907
+ html = compile_method(gfm_compiler, md)
908
+ assert_include html, "切り離された継続行。"
909
+ end
910
+
911
+ def test_gfm_indented_fence_with_title
912
+ md = "# T\n\n- 項目\n ```ruby title=\"例\"\n p 1\n ```\n"
913
+ html = gfm_compiler.compile(md)
914
+ assert_include html, '<span class="caption">例</span>'
915
+ assert_include html, '<pre class="highlight ruby">'
916
+ end
917
+
918
+ # ---- doc / ライブラリページ ----
919
+
920
+ def test_doc_headline_and_paragraph
921
+ assert_equivalent_doc <<~RD
922
+ = タイトル
923
+
924
+ 本文段落。
925
+
926
+ == 節
927
+
928
+ 節の本文。
929
+ RD
930
+ end
931
+
932
+ def test_doc_lists_and_code
933
+ assert_equivalent_doc <<~RD
934
+ = タイトル
935
+
936
+ * 項目1
937
+ * 項目2
938
+
939
+ //emlist[例][ruby]{
940
+ p 1
941
+ //}
942
+ RD
943
+ end
944
+
945
+ def test_doc_dlist
946
+ assert_equivalent_doc <<~RD
947
+ = タイトル
948
+
949
+ : 用語
950
+ 定義文。
951
+ RD
952
+ end
953
+
954
+ # ---- CommonMark 準拠のリスト継続インデント (doctree#3232, znz レビュー) ----
955
+ # https://spec.commonmark.org/0.31.2/#list-items Example 255-258, 264 相当。
956
+ # マーカーの内容カラム幅(「- 」なら2、dlist の dt も常に「- 」なので2)
957
+ # 未満のインデントは、空行を挿むと項目に属さない。ただし空行を挿まない
958
+ # 直接継続(lazy continuation)は CommonMark 同様、幅を問わず許容する。
959
+
960
+ def test_dlist_direct_continuation_allows_shallow_indent
961
+ # 空行なしの直接継続(lazy)はマーカー幅(2)未満でも dd に含まれる
962
+ # (symref.md の実例の大半はこの形)
963
+ html = @md.compile("- **term**:\n description.\n")
964
+ assert_match(%r{<dd>\s*<p>\s*description\.\s*</p>\s*</dd>}m, html)
965
+ end
966
+
967
+ def test_dlist_blank_continuation_with_marker_width_indent_stays_in_dd
968
+ # CommonMark Example 256 相当: マーカー幅(2)以上のインデントなら
969
+ # 空行を挟んだ2段落目も dd に含まれる
970
+ html = @md.compile("- **term**:\n one\n\n two\n")
971
+ assert_include(html, 'one')
972
+ assert_include(html, 'two')
973
+ assert_operator(html.index('two'), :<, html.index('</dd>'))
974
+ end
975
+
976
+ def test_dlist_blank_continuation_with_shallow_indent_breaks_out
977
+ # CommonMark Example 255 相当(symref.md の実例と同型):
978
+ # 空行を挟んだ1スペースだけの継続はマーカー幅(2)未満のため dd に
979
+ # 属さず、dl の外のトップレベル段落になる
980
+ html = @md.compile("- **`2 + 3`**:\n\n 足し算です。\n")
981
+ assert_operator(html.index('</dd>'), :<, html.index('足し算'))
982
+ assert_operator(html.index('</dl>'), :<, html.index('足し算'))
983
+ end
984
+
985
+ def test_dlist_multiple_blank_lines_between_paragraphs
986
+ # CommonMark Example 262 相当: 2行以上の空行を挟んでも
987
+ # インデントが十分なら dd の続きとして扱う
988
+ html = @md.compile("- **term**:\n one\n\n\n two\n")
989
+ assert_operator(html.index('two'), :<, html.index('</dd>'))
990
+ end
991
+
992
+ def test_dlist_fence_after_blank_requires_marker_width
993
+ # フェンスは lazy continuation できない(空行の有無に関わらず
994
+ # マーカー幅以上のインデントが必要)。幅未満なら dd の外の
995
+ # トップレベルフェンスになる
996
+ html = @md.compile("- **term**:\n\n ```\n code\n ```\n")
997
+ assert_operator(html.index('</dd>'), :<, html.index('<pre>'))
998
+ end
999
+
1000
+ def test_item_list_blank_continuation_with_sufficient_indent_stays_nested
1001
+ # CommonMark Example 256 相当・doctree news/*.md の実例型:
1002
+ # 現行実装は空行に当たった時点で項目の外に「脱走」していたが、
1003
+ # マーカー幅以上のインデントなら項目(li)の中に留まる
1004
+ html = @md.compile("- one\n\n two\n")
1005
+ assert_operator(html.index('two'), :<, html.index('</li>'))
1006
+ end
1007
+
1008
+ def test_item_list_blank_continuation_with_insufficient_indent_breaks_out
1009
+ # CommonMark Example 255 相当: マーカー幅未満なら項目の外になる
1010
+ html = @md.compile("- one\n\n two\n")
1011
+ assert_operator(html.index('</li>'), :<, html.index('two'))
1012
+ end
1013
+
1014
+ def test_item_list_fence_after_blank_with_sufficient_indent_stays_nested
1015
+ # news/2_7_0.md 等の実例型:「- 説明文\n\n ```ruby\n...\n ```」の
1016
+ # ようにフェンス例が項目に属したまま描画される(従来は脱走していた)
1017
+ html = @md.compile("- one\n\n ```ruby\n code\n ```\n")
1018
+ assert_operator(html.index('<pre'), :<, html.index('</li>'))
1019
+ end
1020
+
1021
+ def test_item_list_shallow_direct_continuation_still_works
1022
+ # news/2_6_0 型(既存の RD 由来コンテンツ): 空行なしの直接継続は
1023
+ # 項目マーカーより浅いインデントでも項目の継続のまま(回帰確認)
1024
+ assert_equivalent_doc <<~RD
1025
+ = タイトル
1026
+
1027
+ * 項目の一行目が長くて
1028
+ 折り返した継続行。
1029
+
1030
+ 本文。
1031
+ RD
1032
+ end
1033
+
1034
+ def test_ordered_item_list_blank_continuation_requires_marker_width
1035
+ # 番号付きリストはマーカー文字列の幅がマーカーごとに変わる
1036
+ # (「1. 」なら3)。幅未満は空行を挟むと項目の外になる
1037
+ html = @md.compile("1. one\n\n two\n")
1038
+ assert_operator(html.index('</li>'), :<, html.index('two'))
1039
+ wide = @md.compile("1. one\n\n two\n")
1040
+ assert_operator(wide.index('two'), :<, wide.index('</li>'))
1041
+ end
1042
+
1043
+ # ---- インデントコードブロックの検知・警告 (CommonMark Example 264 相当) ----
1044
+ # 言語指定を推奨するため実装は非対応のまま。互換性を壊さず、検知したら
1045
+ # stderr に警告のみ出す
1046
+
1047
+ def test_warns_on_top_level_indented_code_block
1048
+ err = capture_stderr { @md.compile("説明。\n\n code like text\n") }
1049
+ assert_match(/warning/i, err)
1050
+ assert_match(/:\d+/, err) # ファイル名(または識別子):行番号
1051
+ # 描画そのものは変更しない(非互換のまま、警告のみ)
1052
+ html = @md.compile("説明。\n\n code like text\n")
1053
+ assert_match(%r{<p>\s*code like text\s*</p>}m, html)
1054
+ assert_not_match(/<pre>/, html)
1055
+ end
1056
+
1057
+ def test_no_warning_for_shallow_indent_paragraph
1058
+ err = capture_stderr { @md.compile("説明。\n\n two spaces only\n") }
1059
+ assert_equal('', err)
1060
+ end
1061
+
1062
+ def test_no_warning_for_indented_content_consumed_by_list_item
1063
+ # リストの中に正しく取り込まれるインデントは警告対象外
1064
+ # (リスト外・フェンス外のみが対象)
1065
+ err = capture_stderr { @md.compile("- one\n\n four space continuation\n") }
1066
+ assert_equal('', err)
1067
+ end
1068
+
1069
+ def test_no_warning_for_fenced_code_block
1070
+ err = capture_stderr { @md.compile("```ruby\n x = 1\n```\n") }
1071
+ assert_equal('', err)
1072
+ end
1073
+
1074
+ def test_md_ref_module_function_display_typemark
1075
+ # bitclust#282: Markdown ソースの [m:Kernel?.at_exit] は inline 復元時に
1076
+ # ".#" へ正規化される(RD の MethodSpec 互換)が、表示ラベルは DB
1077
+ # バージョンが 4.0 以降なら "?." に戻す。3.4 以前は ".#" のまま。
1078
+ # どちらの表記で書かれていても、ページの見出し表記と常に一致する
1079
+ src = "### def m(v) -> String\n\n" \
1080
+ "[m:Kernel?.at_exit] と [m:Kernel.#at_exit] を参照。\n"
1081
+ { '3.4' => 'Kernel.#at_exit', '4.0' => 'Kernel?.at_exit' }.each do |version, label|
1082
+ db = BitClust::MethodDatabase.dummy("version" => version)
1083
+ md = BitClust::MDCompiler.new(@u, 1, { :database => db })
1084
+ html = compile_method(md, src)
1085
+ assert_equal(2, html.scan(">#{label}</a>").size,
1086
+ "version=#{version}\n#{html}")
1087
+ end
1088
+ end
1089
+
1090
+ # ---- Markdown リンク記法での rurema 参照(ラベル付きリンク) ----
1091
+ # [ラベル](m:spec) のように、リンク宛先へ参照 scheme を書けるようにする。
1092
+ # 呼び出し形を表示しつつメソッドへリンクする用途:
1093
+ # [`OpenSSL::Random.egd_bytes(filename, 255)`](m:OpenSSL::Random?.egd_bytes)
1094
+
1095
+ sub_test_case("md link with rurema reference destination") do
1096
+ def test_ref_link_with_code_span_label
1097
+ html = gfm_compiler.compile("# t\n\n[`Foo#bar(x, y)`](m:Foo#bar) を参照。\n")
1098
+ assert_include html, '<code>Foo#bar(x, y)</code>'
1099
+ assert_include html, 'href="dummy/method/'
1100
+ assert_not_include html, 'class="external"'
1101
+ end
1102
+
1103
+ def test_ref_link_plain_label
1104
+ html = gfm_compiler.compile("# t\n\n[文字列クラス](c:String) を参照。\n")
1105
+ assert_include html, '>文字列クラス</a>'
1106
+ assert_include html, 'href="dummy/class/'
1107
+ end
1108
+
1109
+ def test_ref_link_module_function_spec
1110
+ html = gfm_compiler.compile("# t\n\n[`open(name)`](m:Kernel?.open)\n")
1111
+ assert_include html, '<code>open(name)</code>'
1112
+ assert_include html, 'href="dummy/method/'
1113
+ end
1114
+
1115
+ def test_ref_link_doc_with_fragment_via_ref_scheme
1116
+ html = gfm_compiler.compile("# t\n\n[仕様](ref:d:spec/m17n#anc)\n")
1117
+ assert_include html, '#anc"'
1118
+ assert_include html, '>仕様</a>'
1119
+ end
1120
+
1121
+ def test_ref_link_method_with_fragment_via_ref_scheme
1122
+ html = gfm_compiler.compile("# t\n\n[フォーマット](ref:m:Foo#bar#fmt)\n")
1123
+ assert_include html, '#fmt"'
1124
+ assert_include html, '>フォーマット</a>'
1125
+ assert_include html, 'href="dummy/method/'
1126
+ end
1127
+
1128
+ def test_ref_link_same_page_anchor
1129
+ html = gfm_compiler.compile("# t\n\n[別名](ref:anc)\n")
1130
+ assert_include html, '<a href="#anc">別名</a>'
1131
+ end
1132
+
1133
+ def test_ref_link_lib
1134
+ html = gfm_compiler.compile("# t\n\n[JSON ライブラリ](lib:json)\n")
1135
+ assert_include html, '>JSON ライブラリ</a>'
1136
+ assert_include html, 'href="dummy/library/'
1137
+ end
1138
+
1139
+ def test_ref_link_man
1140
+ html = gfm_compiler.compile("# t\n\n[statx システムコール](man:statx(2linux))\n")
1141
+ assert_include html, 'href="http://man7.org/linux/man-pages/man2/statx.2.html"'
1142
+ assert_include html, '>statx システムコール</a>'
1143
+ assert_include html, 'class="external"'
1144
+ end
1145
+
1146
+ def test_ref_link_man_unresolvable_section_falls_back_to_label
1147
+ html = gfm_compiler.compile("# t\n\n[謎のマニュアル](man:foo(9zzz))\n")
1148
+ assert_include html, '謎のマニュアル'
1149
+ assert_not_include html, '<a '
1150
+ end
1151
+
1152
+ def test_unknown_scheme_stays_plain_text
1153
+ html = gfm_compiler.compile("# t\n\n[x](zzz:y)\n")
1154
+ assert_include html, '[x](zzz:y)'
1155
+ assert_not_include html, '<a '
1156
+ end
1157
+
1158
+ def test_label_is_escaped
1159
+ html = gfm_compiler.compile("# t\n\n[a<b](c:String)\n")
1160
+ assert_include html, '>a&lt;b</a>'
1161
+ end
1162
+
1163
+ def test_ref_link_fires_link_checker
1164
+ recorder = Class.new do
1165
+ attr_reader :refs
1166
+ def initialize; @refs = []; end
1167
+ def note_ref(type, arg); @refs << "#{type}:#{arg}"; end
1168
+ end.new
1169
+ md = BitClust::MDCompiler.new(@u, 1,
1170
+ { :database => @db, :gfm => true, :link_checker => recorder })
1171
+ md.compile("# t\n\n[`bar`](m:Foo#bar) と [s](c:String) と [仕様](ref:d:spec/m17n#anc)\n")
1172
+ assert_include recorder.refs, "m:Foo#bar"
1173
+ assert_include recorder.refs, "c:String"
1174
+ assert_include recorder.refs, "ref:d:spec/m17n#anc"
1175
+ end
1176
+ end
1177
+
1178
+ end
1179
+
1180
+ # lang 指定付きコードブロックのハイライト(ruby は Ripper ベースの
1181
+ # SyntaxHighlighter、その他の言語は Rouge、未知の言語はエスケープのみ)
1182
+ class TestMDCompilerRougeHighlight < Test::Unit::TestCase
1183
+ def setup
1184
+ @dummy = 'dummy'
1185
+ @u = BitClust::URLMapper.new(Hash.new { @dummy })
1186
+ @db = BitClust::MethodDatabase.dummy("version" => "2.0.0")
1187
+ @md = BitClust::MDCompiler.new(@u, 1, { :database => @db, :gfm => true })
1188
+ @rd = BitClust::RDCompiler.new(@u, 1, { :database => @db })
1189
+ end
1190
+
1191
+ def test_c_fence_is_highlighted_with_rouge
1192
+ html = @md.compile("```c\nint x = 1; /* comment */\n```\n")
1193
+ assert_match(/<pre class="highlight c">/, html)
1194
+ assert_match(%r{<span class="kt">int</span>}, html)
1195
+ assert_match(%r{<span class="cm">/\* comment \*/</span>}, html)
1196
+ end
1197
+
1198
+ def test_ruby_alias_rb_uses_bitclust_highlighter
1199
+ ruby_html = @md.compile("```ruby\nputs 1\n```\n")
1200
+ rb_html = @md.compile("```rb\nputs 1\n```\n")
1201
+ assert_equal(ruby_html.sub('highlight ruby', 'highlight rb'), rb_html)
1202
+ # bitclust の SyntaxHighlighter 由来のマークアップ(Rouge の Ruby lexer ではなく)
1203
+ assert_match(%r{<span class="nb">puts</span>}, rb_html)
1204
+ end
1205
+
1206
+ def test_unknown_lang_fence_is_escaped
1207
+ html = @md.compile("```nosuchlang\n<b>&raw</b>\n```\n")
1208
+ assert_match(/<pre class="highlight nosuchlang">/, html)
1209
+ assert_match(/&lt;b&gt;&amp;raw&lt;\/b&gt;/, html)
1210
+ assert_not_match(/<b>/, html)
1211
+ end
1212
+
1213
+ def test_text_fence_is_escaped_without_highlight
1214
+ html = @md.compile("```text\n<b>plain</b>\n```\n")
1215
+ assert_match(/&lt;b&gt;plain&lt;\/b&gt;/, html)
1216
+ assert_not_match(/<b>plain/, html)
1217
+ end
1218
+
1219
+ # info string パースの直接テスト。ハイライト出力経由の検証は Ruby の
1220
+ # パーサ版差(同じ断片でもエラー報告の有無が変わる)で不具合がマスク
1221
+ # されうるため、返り値そのものを固定する
1222
+ class BitClust::MDCompiler; public :parse_fence_info; end
1223
+ data("lang のみ" => ["ruby", ["ruby", nil, false]],
1224
+ "invalid 付き" => ["ruby invalid", ["ruby", nil, true]],
1225
+ "title 付き" => ['ruby title="例"', ["ruby", "例", false]],
1226
+ "invalid + title" => ['ruby invalid title="SyntaxError の例"',
1227
+ ["ruby", "SyntaxError の例", true]],
1228
+ "title 内エスケープ + invalid" => ['ruby invalid title="a\"b"',
1229
+ ["ruby", 'a"b', true]],
1230
+ "未知の余分な語" => ["ruby foo", [nil, nil, false]],
1231
+ "空" => ["", [nil, nil, false]])
1232
+ def test_parse_fence_info(data)
1233
+ rest, expected = data
1234
+ assert_equal(expected, @md.parse_fence_info(rest))
1235
+ end
1236
+
1237
+ # ```ruby invalid: 構文として完全でないコード(SyntaxError の例・文法断片)を
1238
+ # Ripper の構文チェックなしで Rouge の Ruby lexer により色付けする(issue #251)
1239
+ def test_ruby_invalid_fence_uses_rouge_without_syntax_check
1240
+ # Ripper ではパースできない断片でもビルドエラーにならない
1241
+ html = @md.compile("```ruby invalid\nif cond then\n```\n")
1242
+ assert_match(/<pre class="highlight ruby">/, html)
1243
+ assert_match(%r{<span class="k">if</span>}, html)
1244
+ end
1245
+
1246
+ def test_ruby_invalid_fence_with_title
1247
+ html = @md.compile("```ruby invalid title=\"SyntaxError の例\"\ndef broken(\n```\n")
1248
+ assert_match(%r{<span class="caption">SyntaxError の例</span>}, html)
1249
+ assert_match(/<pre class="highlight ruby">/, html)
1250
+ assert_match(%r{<span class="k">def</span>}, html)
1251
+ end
1252
+
1253
+ def test_invalid_flag_on_other_lang_is_harmless
1254
+ plain = @md.compile("```c\nint x = 1;\n```\n")
1255
+ flagged = @md.compile("```c invalid\nint x = 1;\n```\n")
1256
+ assert_equal(plain, flagged)
1257
+ end
1258
+
1259
+ def test_ruby_fence_without_invalid_still_checks_syntax
1260
+ md = BitClust::MDCompiler.new(@u, 1,
1261
+ { :database => @db, :gfm => true, :stop_on_syntax_error => false })
1262
+ html = md.compile("```ruby\nif cond then\n```\n")
1263
+ # invalid なしの ruby は従来どおり構文チェックされ、エラー時は
1264
+ # エスケープのみのフォールバック(色付けなし)になる
1265
+ assert_not_match(/<span class="k">/, html)
1266
+ assert_match(/if cond then/, html)
1267
+ end
1268
+
1269
+ def test_rd_emlist_with_c_lang_is_equivalent
1270
+ rd_src = "//emlist[キャプション][c]{\nint x = 1;\n//}\n"
1271
+ md_src = BitClust::RRDToMarkdown.convert(rd_src)
1272
+ assert_equal(@rd.compile(rd_src), @md.compile(md_src), "md source:\n#{md_src}")
1273
+ assert_match(%r{<span class="kt">int</span>}, @rd.compile(rd_src))
1274
+ end
1275
+
1276
+ end