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,824 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module BitClust
6
+ class MarkdownToRRD
7
+ def self.convert(markdown, capi: false)
8
+ new(markdown, capi: capi).convert
9
+ end
10
+
11
+ # md テキスト断片のインライン記法を rd 形式へ復元する
12
+ # ([type:target] → [[type:target]]、\[ エスケープ解除)。
13
+ # MDCompiler がテキストノードの処理を共有するための公開 API
14
+ def self.restore_inline(text)
15
+ new('').send(:convert_bare_refs, text)
16
+ end
17
+
18
+ # md テキストノードを旧経路と同じ表示形(rd のインライン形式)へ戻す:
19
+ # `__X__` 自動スパン解除、`token` → `token'(GNU 風引用)、
20
+ # 行頭 **N.** → N.、[x:y] → [[x:y]]。
21
+ # MDCompiler の M1 等価描画(compile_text のテキストノード)用。
22
+ # 行頭構造は触らない(テキストノードの行頭 # や - は本文の一部)
23
+ def self.restore_text(text)
24
+ restore_inline(
25
+ text.gsub(/`(__\w+__)`/, '\1')
26
+ .gsub(/(?<!\\)`([^`'\s]+)`/) { "`#{$1}'" }
27
+ .gsub(/^\*\*(\d+\.)\*\* /, '\1 ')
28
+ )
29
+ end
30
+
31
+ # entry#description(meta description 等のコンパイラ非経由テキスト)と
32
+ # RefsDatabase のラベル用: インラインに加えて行頭構造
33
+ # (見出し・@param 系・リスト記号・フェンス・エスケープ)も旧表示形へ戻す
34
+ def self.restore_description(text)
35
+ trailing_newline = text.end_with?("\n")
36
+ saved = [] #: Array[String]
37
+ text = restore_display_fences(text, saved) if text =~ /^`{3,}/
38
+ text = text.lines.map { |l| restore_display_line(l) }.join
39
+ text = text.chomp unless trailing_newline
40
+ # 行頭リスト記号の復元は **N.** 復元より先に行う(復元後の
41
+ # 「N. 」を olist と誤認して (N) 化しないため。DublinCoreModel)
42
+ restore_inline(
43
+ text.gsub(/`(__\w+__)`/, '\1')
44
+ .gsub(/(?<!\\)`([^`'\s]+)`/) { "`#{$1}'" }
45
+ .gsub(/^(\s*)- /, '\1* ')
46
+ .gsub(/^(\s*)(\d+)\. /, '\1(\2) ')
47
+ .gsub(/^\*\*(\d+\.)\*\* /, '\1 ')
48
+ .gsub(/^\\#/, '#')
49
+ .gsub(/\\`/, '`')
50
+ ).gsub(/\x00([-\w]+)\x00/) { "[a:#{$1}]" }
51
+ .gsub(/\x01(\d+)\x01/) { saved[$1.to_i] }
52
+ end
53
+
54
+ # description(段落単位の切り出し)内のフェンスを旧経路の表示形へ戻す:
55
+ # 4+ フェンス(インデントコード由来)→ インデントブロック、
56
+ # ```lang → //emlist[caption][lang]{(前処理後の #@samplecode/emlist 形)。
57
+ # 段落分割で閉じフェンスが切れている形も受ける。
58
+ # フェンス内容はコードなので、後段の行復元・インライン復元から
59
+ # \x01<idx>\x01 プレースホルダで保護し最後に戻す
60
+ def self.restore_display_fences(text, saved)
61
+ protect = lambda do |line, ends_nl|
62
+ saved << line
63
+ "\x01#{saved.size - 1}\x01#{ends_nl ? "\n" : ''}"
64
+ end
65
+ out = +''
66
+ # len と indent(emlist 形は nil)
67
+ fence = nil #: [Integer, Integer | nil]?
68
+ text.each_line do |l|
69
+ nl = l.end_with?("\n")
70
+ if fence
71
+ if l =~ /\A`{#{fence[0]}}\s*$/
72
+ out << protect.call('//}', nl) unless fence[1]
73
+ fence = nil
74
+ elsif l =~ /\A\s*$/
75
+ out << l
76
+ elsif fence[1]
77
+ out << protect.call((' ' * fence[1]) + l.chomp, nl)
78
+ else
79
+ out << protect.call(l.chomp, nl)
80
+ end
81
+ elsif l =~ /\A(`{4,})\s*$/
82
+ len = ($1 || raise).length
83
+ fence = [len, len - 3]
84
+ elsif l =~ /\A(`{3,})(\w+)?(?:\s+title="((?:[^"\\]|\\.)*)")?\s*$/
85
+ len = ($1 || raise).length
86
+ lang = $2
87
+ caption = $3&.gsub(/\\(["\\])/, '\1')
88
+ open = lang ? "//emlist[#{caption}][#{lang}]{" : '//emlist{'
89
+ out << protect.call(open, nl)
90
+ fence = [len, nil]
91
+ else
92
+ out << l
93
+ end
94
+ end
95
+ out
96
+ end
97
+
98
+ # 行頭構造(見出し・メタデータ行)の rd 表示形への復元。
99
+ # 全文変換(convert)の対応箇所と同じ規則の表示専用ミラー
100
+ def self.restore_display_line(l)
101
+ case l
102
+ when /\A(\#{1,6}) (.*?) \{#([-\w]+)\}([ \t]*\n?)\z/m
103
+ # アンカーは restore_inline の裸参照復元([a:x]→[[a:x]])を
104
+ # 避けるため \x00 で包み、最後に [a:x] へ戻す
105
+ "#{'=' * ($1 || raise).length}\x00#{$3}\x00 #{$2}#{$4}"
106
+ when /\A(\#{1,6}) (.*)\z/m
107
+ "#{'=' * ($1 || raise).length} #{$2}"
108
+ when /\A- \*\*(param|arg|raise)\*\*(\s+)`([^`]+)` --(.*)\z/m
109
+ "@#{$1}#{$2}#{$3}#{$4}"
110
+ when /\A- \*\*return\*\* --(.*)\z/m
111
+ "@return#{$1}"
112
+ when /\A- \*\*SEE\*\*(\s*)(.*)\z/m
113
+ "@see#{$1}#{$2}"
114
+ else
115
+ l
116
+ end
117
+ end
118
+
119
+ # capi: C API リファレンスモード。### は見出しではなくシグネチャ
120
+ # 「--- <C sig>」へ復元する(capi に本文見出しは無い)
121
+ def initialize(markdown, capi: false)
122
+ @src = markdown
123
+ @capi = capi
124
+ end
125
+
126
+ def convert
127
+ @lines = @src.lines
128
+ @out = []
129
+ @index = 0
130
+ @front_matter = {}
131
+
132
+ parse_front_matter
133
+ process_body
134
+ @out.join
135
+ end
136
+
137
+ private
138
+
139
+ def parse_front_matter
140
+ return unless @index < @lines.length && @lines[@index] =~ /\A---\s*$/
141
+ advance # skip opening ---
142
+ yaml_lines = [] #: Array[String]
143
+ while @index < @lines.length
144
+ line = current_line
145
+ if line =~ /\A---\s*$/
146
+ advance # skip closing ---
147
+ break
148
+ end
149
+ yaml_lines << line
150
+ advance
151
+ end
152
+ parse_front_matter_raw(yaml_lines)
153
+ emit_library_metadata
154
+ # Skip blank lines after front matter
155
+ advance while @index < @lines.length && current_line =~ /\A\s*\n?\z/
156
+ end
157
+
158
+ def emit_library_metadata
159
+ @out.concat(@library_metadata_body) if @library_metadata_body
160
+ end
161
+
162
+ # front matter を生行で走査し、#@ ディレクティブを保持したまま
163
+ # ライブラリメタ(category/require/sublibrary; H1 前)と
164
+ # クラス関係(include/extend/alias; H1 直後)の body 行を組み立てる。
165
+ # YAML.safe_load は #@ をコメントとして落とすため生行を使う。
166
+ def parse_front_matter_raw(yaml_lines)
167
+ # list key => [[:item, val] | [:dir, line]]
168
+ blocks = {} #: Hash[String?, Array[[Symbol, String]]]
169
+ category = nil
170
+ # ゲート付き category(rd 行の組み立て済み列)
171
+ category_block = nil #: Array[String]?
172
+ cat_depth = 0
173
+ # category ゲート候補のトップレベル #@ 行
174
+ pending = [] #: Array[String]
175
+ # メタ領域先頭の #@# コメント行(irb.rd の Author 行)
176
+ leading = [] #: Array[String]
177
+ key = nil
178
+ yaml_lines.each do |l|
179
+ case l
180
+ when /\A(include|extend|alias|require|sublibrary|library):\s*$/
181
+ # library のゲート付きリスト(多重所属・注入キー)は収集だけして
182
+ # 破棄する(ブロック内の #@ 行が leading に漏れないように)
183
+ key = $1; blocks[key] = []
184
+ when /\Acategory:\s*(.*)$/
185
+ if pending.any?
186
+ # ゲート付き category(cmath 型): #@ 行ごと復元する
187
+ category_block = pending.dup << "category #{($1 || raise).strip}\n"
188
+ cat_depth = pending.count { |p| p =~ /\A\#@(?:since|until|if)\b/ } -
189
+ pending.count { |p| p =~ /\A\#@end\b/ }
190
+ pending = []
191
+ else
192
+ category = ($1 || raise).strip
193
+ end
194
+ key = nil
195
+ when /\A\s+- (.+?)\s*$/
196
+ blocks[key] << [:item, $1 || raise] if key
197
+ when /\A\#@\#/
198
+ key ? blocks[key] << [:dir, l] : leading << l
199
+ when /\A\#@/
200
+ if key
201
+ blocks[key] << [:dir, l]
202
+ elsif category_block && cat_depth > 0
203
+ category_block << l
204
+ cat_depth += 1 if l =~ /\A\#@(?:since|until|if)\b/
205
+ cat_depth -= 1 if l =~ /\A\#@end\b/
206
+ else
207
+ pending << l
208
+ end
209
+ when /\A\S/
210
+ key = nil # その他のトップレベルキー(type/since/until 等)
211
+ end
212
+ end
213
+ leading.concat(pending) # category に消費されなかった #@ 行(通常は無い)
214
+ # クラス関係(H1 直後、RD 文法順 alias → extend → include)
215
+ @class_relations = []
216
+ %w[alias extend include].each do |k|
217
+ next unless blocks[k]
218
+ blocks[k].each { |type, v| @class_relations << (type == :dir ? v : "#{k} #{v}\n") }
219
+ end
220
+ # ライブラリメタ(H1 前)
221
+ @library_metadata_body = []
222
+ unless leading.empty?
223
+ @library_metadata_body.concat(leading) << "\n"
224
+ end
225
+ if category_block
226
+ @library_metadata_body.concat(category_block) << "\n"
227
+ elsif category
228
+ @library_metadata_body << "category #{category}\n" << "\n"
229
+ end
230
+ %w[require sublibrary].each do |k|
231
+ next unless blocks[k]
232
+ blocks[k].each { |type, v| @library_metadata_body << (type == :dir ? v : "#{k} #{v}\n") }
233
+ @library_metadata_body << "\n"
234
+ end
235
+ end
236
+
237
+ def process_body
238
+ while @index < @lines.length
239
+ line = current_line
240
+ case line
241
+ when /\A```/
242
+ convert_code_block(line)
243
+ when /\A### module_function def /
244
+ convert_module_function_signature(line)
245
+ when /\A### def /
246
+ convert_method_signature(line)
247
+ when /\A### const /
248
+ convert_const_signature(line)
249
+ when /\A### gvar /
250
+ convert_gvar_signature(line)
251
+ when /\A### /
252
+ @capi ? convert_capi_signature(line) : convert_h3_heading(line)
253
+ when /\A##### /
254
+ convert_h5_heading(line)
255
+ when /\A#### /
256
+ convert_h4_heading(line)
257
+ when /\A- \*\*(param|return|raise)\*\*/
258
+ convert_metadata(line, $1)
259
+ when /\A- \*\*SEE\*\*/
260
+ convert_see_list_upper(line)
261
+ when /\A- \*\*see\*\*/
262
+ convert_see_list(line)
263
+ when /\A\*\*SEE\*\*/
264
+ convert_see(line)
265
+ when /\A@see /
266
+ convert_see_passthrough(line)
267
+ when /\A## /
268
+ convert_h2(line)
269
+ when /\A- \*\*(?!param\b|return\b|raise\b|see\b)(.+?)\*\*:/
270
+ convert_dlist_colon_item(line)
271
+ when /\A- \*\*(?!param\b|return\b|raise\b|see\b)(\w+)\*\* -- /
272
+ convert_dlist_item(line)
273
+ when /\A\*\*(\d+)\.\*\*\s/
274
+ convert_bold_number(line)
275
+ when /\A(\s*)- /
276
+ convert_list_item(line, $1 || raise)
277
+ when /\A(\s{0,3})(\d+)\.\s/
278
+ convert_ordered_list_item(line, $2.to_i, $1 || raise)
279
+ when /\A:\s/
280
+ convert_dlist_passthrough(line)
281
+ when /\A {4,}/
282
+ convert_indented_code(line)
283
+ when /\A\#@/
284
+ raw_passthrough(line)
285
+ when /\A\\#/
286
+ convert_escaped_hash_line(line)
287
+ when /\A# /
288
+ convert_h1(line)
289
+ else
290
+ passthrough(line)
291
+ end
292
+ end
293
+ end
294
+
295
+ def convert_code_block(line)
296
+ # バッククォートの個数を取得
297
+ line =~ /\A(`{3,})/
298
+ fence = $1 || raise
299
+ fence_len = fence.length
300
+
301
+ # 4個以上のバッククォート(言語指定なし)→ インデントコードに復元
302
+ if fence_len > 3 && line =~ /\A`{4,}\s*$/
303
+ indent = ' ' * (fence_len - 3)
304
+ advance
305
+ while @index < @lines.length
306
+ l = current_line
307
+ if l =~ /\A`{#{fence_len}}\s*$/
308
+ advance
309
+ return
310
+ end
311
+ if l =~ /\A\s*$/
312
+ @out << l
313
+ else
314
+ @out << indent + l
315
+ end
316
+ advance
317
+ end
318
+ return
319
+ end
320
+
321
+ title = nil
322
+ lang = nil
323
+ fence_len = 3
324
+ if line =~ /\A(`{3,})(\w*)/
325
+ fence_len = ($1 || raise).length
326
+ lang = ($2 || raise).empty? ? nil : $2
327
+ end
328
+ if line =~ /title="((?:[^"\\]|\\.)*)"/
329
+ title = ($1 || raise).gsub(/\\(["\\])/, '\1')
330
+ end
331
+
332
+ if lang == 'ruby'
333
+ # Ruby → #@samplecode
334
+ samplecode = '#@samplecode'
335
+ samplecode_end = '#@end'
336
+ @out << (title && !title.empty? ? "#{samplecode} #{title}\n" : "#{samplecode}\n")
337
+ else
338
+ # Other language or unspecified → //emlist
339
+ parts = ['//emlist']
340
+ if title && !title.empty?
341
+ parts << "[#{title}]"
342
+ parts << "[#{lang}]" if lang
343
+ elsif lang
344
+ parts << '[]'
345
+ parts << "[#{lang}]"
346
+ end
347
+ parts << "{"
348
+ @out << parts.join + "\n"
349
+ end
350
+ advance
351
+ while @index < @lines.length
352
+ line = current_line
353
+ if line =~ /\A`{#{fence_len}}\s*\n?\z/
354
+ if lang == 'ruby'
355
+ @out << "#{samplecode_end}\n"
356
+ else
357
+ @out << "//}\n"
358
+ end
359
+ advance
360
+ return
361
+ end
362
+ # No cross-reference conversion inside code blocks
363
+ @out << line
364
+ advance
365
+ end
366
+ end
367
+
368
+ # dd/@param 系の説明内にある、インデントされたフェンス(rd では
369
+ # //emlist・#@samplecode の桁0出力に対応。CommonMark 準拠のため
370
+ # rrd_to_markdown.rb が項目の内容カラムへインデントして出すように
371
+ # なった分の桁0版)を、桁0の //emlist{ / #@samplecode に戻す。
372
+ # convert_code_block の 3 バッククォート(lang 付き)分岐と対称。
373
+ # 4+ バッククォート(インデントコード由来)のインデント版は
374
+ # rrd_to_markdown.rb が dd/@param からその形式を出さないため未対応
375
+ def convert_indented_code_block(line, indent)
376
+ line =~ /\A[ \t]+(`{3,})(\w*)(?:\s+title="((?:[^"\\]|\\.)*)")?\s*$/ or raise
377
+ fence_len = ($1 || raise).length
378
+ lang = ($2 || raise).empty? ? nil : $2
379
+ title = $3&.gsub(/\\(["\\])/, '\1')
380
+ strip_re = /\A[ \t]{1,#{indent.length}}/
381
+
382
+ if lang == 'ruby'
383
+ samplecode = '#@samplecode'
384
+ samplecode_end = '#@end'
385
+ @out << (title && !title.empty? ? "#{samplecode} #{title}\n" : "#{samplecode}\n")
386
+ else
387
+ parts = ['//emlist']
388
+ if title && !title.empty?
389
+ parts << "[#{title}]"
390
+ parts << "[#{lang}]" if lang
391
+ elsif lang
392
+ parts << '[]'
393
+ parts << "[#{lang}]"
394
+ end
395
+ parts << "{"
396
+ @out << parts.join + "\n"
397
+ end
398
+ advance
399
+ terminator = /\A[ \t]{0,#{indent.length}}`{#{fence_len}}\s*\n?\z/
400
+ while @index < @lines.length
401
+ l = current_line
402
+ if l =~ terminator
403
+ @out << (lang == 'ruby' ? "#{samplecode_end}\n" : "//}\n")
404
+ advance
405
+ return
406
+ elsif l =~ /\A\#@/
407
+ @out << l # ディレクティブはカラム0のまま(rrd_to_markdown.rb 側と対称)
408
+ else
409
+ @out << l.sub(strip_re, '')
410
+ end
411
+ advance
412
+ end
413
+ end
414
+
415
+ def convert_method_signature(line)
416
+ @out << line.sub(/\A### def /, '--- ')
417
+ advance
418
+ end
419
+
420
+ def convert_module_function_signature(line)
421
+ @out << line.sub(/\A### module_function def /, '--- ')
422
+ advance
423
+ end
424
+
425
+ def convert_const_signature(line)
426
+ @out << line.sub(/\A### const /, '--- ')
427
+ advance
428
+ end
429
+
430
+ def convert_gvar_signature(line)
431
+ @out << line.sub(/\A### gvar /, '--- ')
432
+ advance
433
+ end
434
+
435
+ def convert_metadata(line, kind)
436
+ case kind
437
+ when 'param'
438
+ if (m = /\A- \*\*param\*\*(\s+)`([^`]+)` --(.*)$/.match(line))
439
+ @out << convert_inline_refs("@param#{m[1]}#{m[2]}#{m[3]}\n")
440
+ end
441
+ when 'raise'
442
+ if (m = /\A- \*\*raise\*\*(\s+)`([^`]+)` --(.*)$/.match(line))
443
+ @out << convert_inline_refs("@raise#{m[1]}#{m[2]}#{m[3]}\n")
444
+ end
445
+ when 'return'
446
+ if (m = /\A- \*\*return\*\*(?: `[^`]+`)? --(.*)$/.match(line))
447
+ @out << convert_inline_refs("@return#{m[1]}\n")
448
+ end
449
+ end
450
+ advance
451
+ collect_md_continuation_lines(greedy: true)
452
+ end
453
+
454
+ # greedy: rd 側の @param 系継続(RDCompiler の dd_without_p 相当)と対称。
455
+ # 空白のみの行も継続として保持し、完全な空行でのみ停止する
456
+ def collect_md_continuation_lines(greedy: false)
457
+ nest = 0
458
+ while @index < @lines.length
459
+ line = current_line
460
+ if line =~ /\A\#@(?:since|until|if)\b/
461
+ nest += 1
462
+ raw_passthrough(line)
463
+ elsif line =~ /\A\#@else/
464
+ raw_passthrough(line)
465
+ elsif line =~ /\A\#@end/
466
+ # rd 側と対称: nest 0 の #@end も透過(fileutils の @param 継続)
467
+ nest -= 1 if nest > 0
468
+ raw_passthrough(line)
469
+ elsif line =~ /\A\s+\S/ && line !~ /\A- / && line !~ /\A\*\*/ && line !~ /\A[ \t]*`{3,}/
470
+ @out << convert_inline_refs(line)
471
+ advance
472
+ elsif greedy && line =~ /\A[ \t]+$/
473
+ @out << line
474
+ advance
475
+ elsif greedy && line =~ /\A`{3,}/
476
+ convert_code_block(line) # rd 側と対称: 直結するコードブロックは説明の一部
477
+ elsif greedy && line =~ /\A([ \t]+)`{3,}/
478
+ convert_indented_code_block(line, $1 || raise) # 上と同じくインデント版
479
+ else
480
+ break
481
+ end
482
+ end
483
+ end
484
+
485
+ def convert_see(line)
486
+ # **SEE** → @see、[→[[ 置換、区切りは保持
487
+ rest = line.sub(/\A\*\*SEE\*\*(\s*)/, '').chomp
488
+ space = $1
489
+ @out << "@see#{space}#{convert_inline_refs(rest)}\n"
490
+ advance
491
+ end
492
+
493
+ def convert_see_list_upper(line)
494
+ # - **SEE** [ref] → @see [[ref]]、区切り保持
495
+ rest = line.sub(/\A- \*\*SEE\*\*(\s*)/, '').chomp
496
+ space = $1
497
+ @out << "@see#{space}#{convert_inline_refs(rest)}\n"
498
+ advance
499
+ collect_md_continuation_lines
500
+ end
501
+
502
+ def convert_see_list(line)
503
+ # - **see** [display][type:ref] → @see [[type:ref]]
504
+ rest = line.sub(/\A- \*\*see\*\*\s*/, '').chomp
505
+ @out << "@see #{convert_inline_refs(rest)}\n"
506
+ advance
507
+ end
508
+
509
+ def convert_see_passthrough(line)
510
+ # @see [m:A], [m:B] → @see [[m:A]], [[m:B]] (区切り保持)
511
+ rest = line.sub(/\A@see\s+/, '').chomp
512
+ @out << "@see #{convert_inline_refs(rest)}\n"
513
+ advance
514
+ end
515
+
516
+ def convert_heading_with_anchor(line, prefix)
517
+ if line =~ /\{#([^}]+)\}\s*$/
518
+ anchor = $1
519
+ text = line.sub(/\A#+\s+/, '').sub(/\s*\{#[^}]+\}\s*$/, '')
520
+ @out << "#{prefix}[a:#{anchor}] #{restore_heading_backticks(text)}\n"
521
+ else
522
+ text = line.sub(/\A#+\s+/, '').chomp
523
+ @out << "#{prefix} #{restore_heading_backticks(text)}\n"
524
+ end
525
+ advance
526
+ end
527
+
528
+ # 見出しテキストのバッククォート復元。参照変換(convert_bare_refs)は
529
+ # 通さない(rd→md も見出しでは参照エスケープをしないため対称に)
530
+ def restore_heading_backticks(text)
531
+ strip_code_spans(text).gsub(/\\`/, '`')
532
+ end
533
+
534
+ def convert_h3_heading(line)
535
+ convert_heading_with_anchor(line, '===')
536
+ end
537
+
538
+ def convert_h4_heading(line)
539
+ convert_heading_with_anchor(line, '====')
540
+ end
541
+
542
+ def convert_h5_heading(line)
543
+ # プレフィックス置換のみ(rd 側と対称、空白を保持)
544
+ @out << line.sub(/\A##### /, '===== ')
545
+ advance
546
+ end
547
+
548
+ def strip_code_span(text)
549
+ text.sub(/\A`(.+)`\z/, '\\1')
550
+ end
551
+
552
+ def convert_dlist_colon_item(line)
553
+ # - **`term`**: description → : term\n description
554
+ # - **term**:\n line1\n line2 → : term\n line1\n line2
555
+ if line =~ /\A- \*\*(.+?)\*\*: (.+)$/
556
+ # 構造の `term` は GNU 引用復元より先に unwrap する
557
+ @out << ": #{convert_inline_refs(strip_code_span($1 || raise))}\n #{convert_inline_refs($2 || raise)}\n"
558
+ advance
559
+ elsif line =~ /\A- \*\*(.+?)\*\*:\s*$/
560
+ @out << ": #{convert_inline_refs(strip_code_span($1 || raise))}\n"
561
+ advance
562
+ # 継続行を収集(空行を挟む場合も含む)。rd 側と対称:
563
+ # RDCompiler の dd_with_p はインデント段落とコードブロックを
564
+ # 交互に何個でも説明として受ける(String#% 型)
565
+ while @index < @lines.length
566
+ l = current_line
567
+ if l =~ /\A\s+\S/ && l !~ /\A[ \t]+`{3,}/
568
+ @out << convert_inline_refs(l)
569
+ advance
570
+ elsif l =~ /\A`{3,}/
571
+ convert_code_block(l)
572
+ elsif l =~ /\A([ \t]+)`{3,}/
573
+ convert_indented_code_block(l, $1 || raise) # 上と同じくインデント版
574
+ elsif l =~ /\A\#@/
575
+ # rd 側と対称: #@ 指令行は dd の文脈に透明
576
+ raw_passthrough(l)
577
+ elsif l =~ /\A\s*$/
578
+ # rd 側と対称: 透明なのは版解決で消える指令のみ(#@include は停止要因)
579
+ scan = @index + 1
580
+ scan += 1 while scan < @lines.length &&
581
+ (@lines[scan] =~ /\A\s*$/ ||
582
+ (@lines[scan] =~ /\A\#@/ && @lines[scan] !~ /\A\#@include/))
583
+ nxt = scan < @lines.length ? @lines[scan] : nil
584
+ if nxt && nxt !~ /\A- \*\*/ && (nxt =~ /\A\s+\S/ || nxt =~ /\A`{3,}/)
585
+ @out << l
586
+ advance
587
+ else
588
+ break
589
+ end
590
+ else
591
+ break
592
+ end
593
+ end
594
+ else
595
+ advance
596
+ end
597
+ end
598
+
599
+ def convert_dlist_item(line)
600
+ if line =~ /\A- \*\*(.+?)\*\* -- (.*)$/
601
+ @out << ": #{$1}\n #{convert_inline_refs($2 || raise)}\n"
602
+ end
603
+ advance
604
+ end
605
+
606
+ def convert_bold_number(line)
607
+ # **N.** text → N. text (太字番号テキスト → RD のテキスト)
608
+ @out << convert_inline_refs(line.sub(/\A\*\*(\d+\.)\*\*\s/, '\\1 '))
609
+ advance
610
+ end
611
+
612
+ def convert_ordered_list_item(line, num, indent)
613
+ rrd_indent = indent.empty? ? ' ' : indent
614
+ @out << convert_inline_refs(line.sub(/\A\s*\d+\. /, "#{rrd_indent}(#{num}) "))
615
+ advance
616
+ # 継続行を収集(convert_list_item と同じ、rd 側と対称)
617
+ while @index < @lines.length
618
+ l = current_line
619
+ if l =~ /\A\#@/
620
+ raw_passthrough(l)
621
+ elsif l =~ /\A\s*$/
622
+ break
623
+ elsif l =~ /\A(\s+)\S/ && l !~ /\A\s*-\s/ && l !~ /\A\s*\d+\.\s/
624
+ @out << convert_inline_refs(l)
625
+ advance
626
+ else
627
+ break
628
+ end
629
+ end
630
+ end
631
+
632
+ def convert_list_item(line, indent)
633
+ rrd_indent = indent.empty? ? ' ' : indent
634
+ line =~ /\A\s*-(\s+)/
635
+ content_indent = (indent.empty? ? 1 : indent.length) + 1 + ($1 || raise).length
636
+ @out << convert_inline_refs(line.sub(/\A\s*-(\s+)/, "#{rrd_indent}*\\1"))
637
+ advance
638
+ # 継続行を収集(空行・リスト項目・#@ で停止)。
639
+ # rd 側と対称: 継続はインデント深さ不問(RDCompiler の項目継続規則)
640
+ while @index < @lines.length
641
+ l = current_line
642
+ if l =~ /\A\#@/
643
+ raw_passthrough(l)
644
+ elsif l =~ /\A\s*$/
645
+ break
646
+ elsif l =~ /\A(\s+)\S/ && l !~ /\A\s*-\s/ && l !~ /\A\s*\d+\.\s/
647
+ @out << convert_inline_refs(l)
648
+ advance
649
+ else
650
+ break
651
+ end
652
+ end
653
+ end
654
+
655
+ def convert_dlist_passthrough(line)
656
+ # `: term` + 説明行をそのままパススルー
657
+ passthrough(line)
658
+ while @index < @lines.length
659
+ l = current_line
660
+ if l =~ /\A\s+\S/
661
+ passthrough(l)
662
+ else
663
+ break
664
+ end
665
+ end
666
+ end
667
+
668
+ def convert_indented_code(line)
669
+ # C1: インデント -3 して出力
670
+ # 最初の行が実際にコードでなければパススルー
671
+ unless current_line =~ /\A( {4,})\S/
672
+ passthrough(current_line)
673
+ return
674
+ end
675
+ while @index < @lines.length
676
+ line = current_line
677
+ if line =~ /\A( {4,})\S/
678
+ n = ($1 || raise).length
679
+ deindent = [n - 3, 1].max
680
+ @out << (' ' * deindent) + (line[n..] || raise)
681
+ advance
682
+ elsif line =~ /\A\s*$/
683
+ if @index + 1 < @lines.length && @lines[@index + 1] =~ /\A {4,}\S/
684
+ @out << line
685
+ advance
686
+ else
687
+ break
688
+ end
689
+ else
690
+ break
691
+ end
692
+ end
693
+ end
694
+
695
+ def convert_h2(line)
696
+ @out << line.sub(/\A## /, '== ')
697
+ advance
698
+ end
699
+
700
+ # capi の C シグネチャ(キーワード無し ###)を復元する
701
+ def convert_capi_signature(line)
702
+ @out << line.sub(/\A### /, '--- ')
703
+ advance
704
+ end
705
+
706
+ # rd→md がエスケープした行頭 # のリテラル本文を復元する
707
+ def convert_escaped_hash_line(line)
708
+ @out << convert_inline_refs(line.sub(/\A\\/, ''))
709
+ advance
710
+ end
711
+
712
+ def convert_h1(line)
713
+ return convert_heading_with_anchor(line, '=') if line =~ /\{#[^}]+\}\s*$/
714
+ @out << line.sub(/\A# /, '= ')
715
+ advance
716
+ if @class_relations && !@class_relations.empty?
717
+ @out.concat(@class_relations)
718
+ @class_relations = []
719
+ end
720
+ end
721
+
722
+ def raw_passthrough(line)
723
+ @out << line
724
+ advance
725
+ end
726
+
727
+ def passthrough(line)
728
+ @out << convert_inline_refs(line)
729
+ advance
730
+ end
731
+
732
+ # テキスト行のコードスパンを rd の元表記へ復元する:
733
+ # - `__WORD__`(自動スパン)→ __WORD__
734
+ # - `token`(GNU 風引用由来)→ `token'
735
+ # エスケープ済み \` はスパン開始とみなさない(convert_bare_refs が後で復元)
736
+ def strip_code_spans(text)
737
+ text.gsub(/`(__\w+__)`/, '\\1')
738
+ .gsub(/(?<!\\)`([^`'\s]+)`/) { "`#{$1}'" }
739
+ end
740
+
741
+ # Markdown のブラケットリンク: エスケープされた \[ \] を含むパターン
742
+ def convert_inline_refs(line)
743
+ # コードスパンの復元(`__X__`・GNU 風引用)は \` の解除より先。
744
+ # 続けて bare [type:target] → [[type:target]] (手動パースで \[\] 対応)
745
+ convert_bare_refs(strip_code_spans(line))
746
+ end
747
+
748
+ def convert_bare_refs(line)
749
+ result = +""
750
+ i = 0
751
+ while i < line.length
752
+ if line[i] == '\\' && line[i + 1] == '['
753
+ # rd→md がエスケープしたリテラル [x:y](参照ではない)→ そのまま復元
754
+ result << '['
755
+ i += 2
756
+ next
757
+ end
758
+ if line[i] == '\\' && line[i + 1] == '`'
759
+ # rd→md がエスケープした生バッククォート → そのまま復元
760
+ result << '`'
761
+ i += 2
762
+ next
763
+ end
764
+ if line[i] == '[' && (i == 0 || line[i-1] != '[') && (i + 1 >= line.length || line[i+1] != '[')
765
+ # [ の開始を検出([[ は除外)— エスケープを考慮して ] を探す
766
+ j = find_closing_bracket(line, i + 1)
767
+ if j
768
+ inner = line[i+1...j] || raise
769
+ if inner =~ /\A[a-zA-Z][a-zA-Z-]*:/
770
+ result << convert_md_ref_to_rrd(inner)
771
+ i = j + 1
772
+ next
773
+ end
774
+ end
775
+ end
776
+ result << (line[i] || raise)
777
+ i += 1
778
+ end
779
+ result
780
+ end
781
+
782
+ def find_closing_bracket(line, start)
783
+ j = start
784
+ while j < line.length
785
+ if line[j] == '\\' && j + 1 < line.length && (line[j+1] == '[' || line[j+1] == ']' || line[j+1] == '\\')
786
+ j += 2
787
+ elsif line[j] == ']'
788
+ return j
789
+ else
790
+ j += 1
791
+ end
792
+ end
793
+ nil
794
+ end
795
+
796
+ def convert_md_ref_to_rrd(ref)
797
+ # Markdown の \[ \] \` \\ → RD の [ ] ` \ 。
798
+ # \` の復元は convert_bare_refs のトップレベル分岐([...] の外)と
799
+ # 対称: [m:$\`](バッククォートをエスケープした参照)も
800
+ # [m:$`](素のバッククォート)と同じ [[m:$`]] へ復元する
801
+ # (rurema/doctree#3232 レビュー、MDCompiler#extract_code_spans が
802
+ # コードスパンの開始候補から \` を除外するのと同じ理由で、
803
+ # ブラケット内の \` もコードスパンの巻き添えにならない)
804
+ unescaped = ref.gsub('\\[', '[').gsub('\\]', ']').gsub('\\`', '`').gsub('\\\\', '\\')
805
+ # ? → .# (モジュール関数参照)
806
+ unescaped = unescaped.sub(/\?\./, '.#')
807
+ # RD で [] で終わるメソッド名のみ末尾スペースが必要 ([[m:Hash#[] ]])
808
+ # []= のように [] の後に文字がある場合はスペース不要 ([[m:String#[]=]])
809
+ if unescaped.end_with?('[]')
810
+ "[[#{unescaped} ]]"
811
+ else
812
+ "[[#{unescaped}]]"
813
+ end
814
+ end
815
+
816
+ def current_line
817
+ @lines[@index]
818
+ end
819
+
820
+ def advance
821
+ @index += 1
822
+ end
823
+ end
824
+ end