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,883 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/mdcompiler.rb
4
+ #
5
+ # Markdown ソース → HTML のネイティブコンパイラ(Markdown 移行フェーズ3)。
6
+ #
7
+ # RDCompiler のサブクラスとして、行レベルのディスパッチだけを Markdown 記法
8
+ # (MARKUP_SPEC)に差し替える。HTML の出力部品(見出し・リスト・dl・
9
+ # シンタックスハイライト・リンク解決)はすべて RDCompiler から継承する。
10
+ #
11
+ # M1(等価モード、既定): 変換器が生成する md に対して、対応する rd を
12
+ # RDCompiler にかけた場合と同一の HTML を出力する(tools/md-compile-check.rb
13
+ # が全実データで検証する)。
14
+ #
15
+ # M2(GFM モード、option :gfm => true): GFM の表現を描画する —
16
+ # インラインコードスパン `x` → <code>、行頭 **N.** → <strong>、
17
+ # GFM テーブル(ヘッダ + 区切り行必須)→ <table>。
18
+ # M1 との差は <code>/<strong>/<table> 系マークアップのみ
19
+ # (tools/md-compile-check.rb --gfm が全実データで検証する)。
20
+
21
+ require 'bitclust/rdcompiler'
22
+ require 'bitclust/markdown_to_rrd'
23
+
24
+ module BitClust
25
+
26
+ class MDCompiler < RDCompiler
27
+
28
+ private
29
+
30
+ # RDCompiler のテキストコンパイル(エスケープ・参照解決)を
31
+ # GFM モードのセグメント処理から呼ぶための別名
32
+ alias rd_compile_text compile_text
33
+
34
+ def gfm?
35
+ @option[:gfm]
36
+ end
37
+
38
+ # メソッド系シグネチャ(キーワード付き h3)。capi(@type == :function)は
39
+ # キーワード無しの ### 全部がシグネチャ(capi に本文見出しは無い)
40
+ METHOD_SIGNATURE_RE = /\A### (?:module_function def |def |const |gvar )/
41
+
42
+ def signature_re
43
+ @type == :function ? /\A### / : METHOD_SIGNATURE_RE
44
+ end
45
+
46
+ # $1: 行頭の空白(ネストレベル判定用)、$2: マーカー文字列そのもの
47
+ # ("- " や "12. " 等。CommonMark の項目内容カラム幅の算出に使う。
48
+ # spec 0.31.2 #list-items)
49
+ MD_ITEM_RE = /\A(\s*)(- |\d+\. )/
50
+ FENCE_RE = /\A`{3,}/
51
+ # インデントされたフェンス(リスト項目・dlist 説明内のコードブロック)
52
+ INDENTED_FENCE_RE = /\A([ \t]+)(`{3,})/
53
+ # dd 段落の継続行(インデント行。ただしインデントフェンスの手前で止まる)
54
+ DD_TEXT_RE = /\A[ \t](?![ \t]*`{3})/
55
+ # 用語の後ろに {#id} があればアンカー id として扱う(用語集の各用語への
56
+ # リンク用。rurema/doctree#2634)。id は $2 に入る。
57
+ DLIST_RE = /\A- \*\*(.+?)\*\*:(?:[ \t]*\{#([\w-]+)\})?(?:\s|$)/
58
+ # dlist の dt は常に「- 」マーカー(幅2)。CommonMark の項目内容カラム
59
+ # (spec 0.31.2 Example 255-258): 空行を挟んだ継続はこの幅以上の
60
+ # インデントが無いと dd に属さない(#3232, znz レビュー)
61
+ DLIST_CONTENT_WIDTH = 2
62
+ # CommonMark のインデントコードブロック相当の桁数(言語指定が付けられる
63
+ # フェンスを推奨するため、この実装では検知して警告するのみで非対応)
64
+ INDENTED_CODE_BLOCK_WIDTH = 4
65
+ INFO_RE = /\A- \*\*(?:param|arg|return|raise)\*\*/
66
+ SEE_RE = /\A- \*\*SEE\*\*/
67
+ # @undef など変換器が生のまま渡す未知メタデータ
68
+ # (RDCompiler の entry_info ループ条件 /\A@(?!see)\w+/ と同じ)
69
+ RAW_META_RE = /\A@(?!see)\w+/
70
+ # 生のまま残った #@samplecode が前処理で //emlist になったもの
71
+ EMLIST_LEFTOVER_RE = %r<\A//emlist(?:\[(?:[^\[\]]+?)?\]\[\w+?\])?\{>
72
+
73
+ def library_file
74
+ while @f.next?
75
+ case @f.peek
76
+ when signature_re
77
+ entry_chunk
78
+ when /\A\\?#/
79
+ if /\A\\#/ =~ @f.peek
80
+ paragraph # 行頭 # のリテラル本文(エスケープ済み)
81
+ else
82
+ headline @f.gets || raise
83
+ end
84
+ when SEE_RE
85
+ # findings#1: doc/lib ページの @see も SEE_ALSO(rd 側と同期)
86
+ see
87
+ when DLIST_RE
88
+ dlist
89
+ when MD_ITEM_RE
90
+ @item_stack = []
91
+ item_list(($1 || raise).size)
92
+ raise "@item_stack should be empty. #{@item_stack.inspect}" unless @item_stack.empty?
93
+ when FENCE_RE
94
+ code_fence
95
+ when INDENTED_FENCE_RE
96
+ indented_code_fence
97
+ when EMLIST_LEFTOVER_RE
98
+ # リスト脈絡などで生のまま残った #@samplecode は前処理で
99
+ # //emlist になる。RDCompiler と同じ独立ブロックとして描画する
100
+ emlist
101
+ when /\A\s*\|/
102
+ paragraph unless try_table
103
+ else
104
+ if @f.peek&.strip&.empty?
105
+ @f.gets
106
+ else
107
+ warn_if_indented_code_block(@f.peek || raise)
108
+ paragraph
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ def entry_chunk
115
+ @out.puts '<dl>' if @option[:force]
116
+ first = true
117
+ attrs = [] #: Array[String]
118
+ while @f.next?
119
+ if signature_re =~ @f.peek
120
+ method_signature(@f.gets || raise, first)
121
+ first = false
122
+ elsif !first && METHOD_ATTRIBUTE_LINE_RE =~ @f.peek
123
+ # メソッド属性行は本文には描画しない(undef のみ後でメッセージ)
124
+ attrs.concat attribute_tokens(@f.gets)
125
+ else
126
+ break
127
+ end
128
+ end
129
+ @out.puts %Q(<dd class="#{@type.to_s}-description">)
130
+ undef_message if attrs.include?('undef')
131
+ while @f.next?
132
+ case @f.peek
133
+ when signature_re
134
+ break
135
+ when /\A\#{3,}\s/
136
+ headline @f.gets || raise
137
+ when /\A\\#/
138
+ entry_paragraph # エスケープされた行頭 # リテラル
139
+ when /\A\#{1,2}\s/
140
+ if @option[:force]
141
+ break
142
+ else
143
+ raise "#{@type.to_s} entry includes headline: #{@f.peek.inspect}"
144
+ end
145
+ when SEE_RE
146
+ see
147
+ when INFO_RE
148
+ entry_info
149
+ when DLIST_RE
150
+ dlist
151
+ when MD_ITEM_RE
152
+ @item_stack = []
153
+ item_list(($1 || raise).size)
154
+ raise "@item_stack should be empty. #{@item_stack.inspect}" unless @item_stack.empty?
155
+ when FENCE_RE
156
+ code_fence
157
+ when INDENTED_FENCE_RE
158
+ indented_code_fence
159
+ when EMLIST_LEFTOVER_RE
160
+ emlist
161
+ when /\A@todo\b/
162
+ # findings#3: rd 側と同じく行頭アンカー付きで
163
+ todo
164
+ when RAW_META_RE
165
+ entry_info
166
+ when /\A\s*\|/
167
+ entry_paragraph unless try_table
168
+ else
169
+ if @f.peek&.strip&.empty?
170
+ @f.gets
171
+ else
172
+ warn_if_indented_code_block(@f.peek || raise)
173
+ entry_paragraph
174
+ end
175
+ end
176
+ end
177
+ @out.puts '</dd>'
178
+ @out.puts '</dl>' if @option[:force]
179
+ end
180
+
181
+ # md シグネチャ行を rd 形式(--- ...)へ落とし、既存のシグネチャ処理
182
+ # (MethodSignature.parse・permalink・edit link)を継承する
183
+ def method_signature(sig_line, first)
184
+ super(sig_line.sub(signature_re, '--- '), first)
185
+ end
186
+
187
+ def headline(line)
188
+ hashes = line[/\A#+/] || raise
189
+ label = line.sub(/\A#+\s*/, '').strip
190
+ frag = nil
191
+ if label =~ /\{#([^}]+)\}\z/
192
+ frag = $1
193
+ label = label.sub(/\s*\{#[^}]+\}\z/, '')
194
+ end
195
+ level = @hlevel + (hashes.size - 3)
196
+ line h(level, escape_html(restore_rd_text(label)), frag)
197
+ end
198
+
199
+ def read_paragraph(f)
200
+ f.span(%r{\A(?!\#|`{3}|- |\d+\. |//emlist(?:\[(?:[^\[\]]+?)?\]\[\w+?\])?\{)\S|\A\\#})
201
+ end
202
+
203
+ def read_entry_paragraph(f)
204
+ f.span(%r{\A(?!\#|`{3}|- |\d+\. |@[a-z]|//emlist(?:\[(?:[^\[\]]+?)?\]\[\w+?\])?\{)\S|\A\\#})
205
+ end
206
+
207
+ def paragraph
208
+ line '<p>'
209
+ line compile_text(text_node_from_lines(consume_paragraph_lines { read_paragraph(@f) }))
210
+ line '</p>'
211
+ end
212
+
213
+ def entry_paragraph
214
+ line '<p>'
215
+ line compile_text(text_node_from_lines(consume_paragraph_lines { read_entry_paragraph(@f) }))
216
+ line '</p>'
217
+ end
218
+
219
+ # 段落行の読み取り。リストと空行で切り離された残余のインデント行は
220
+ # どのブロック処理にも該当しないため、ここで段落として消費する
221
+ # (読み取りが空のままだとディスパッチが進まなくなる)
222
+ def consume_paragraph_lines
223
+ lines = yield.map { |l| unescape_hash(l) }
224
+ lines = @f.span(DD_TEXT_RE) if lines.empty?
225
+ lines = [@f.gets || raise] if lines.empty? # 最低1行は必ず進める
226
+ lines
227
+ end
228
+
229
+ def unescape_hash(line)
230
+ line.sub(/\A\\#/, '#')
231
+ end
232
+
233
+ # - **param** `name` -- desc / - **return** -- desc / - **raise** `Ex` -- desc
234
+ # および生の @xxx(未知メタデータ、RDCompiler と同じく UNKNOWN_META_INFO)
235
+ def entry_info
236
+ line '<dl>'
237
+ while @f.next? and INFO_RE =~ @f.peek || RAW_META_RE =~ @f.peek || /\A$/ =~ @f.peek
238
+ header = @f.gets or raise
239
+ next if /\A$/ =~ header
240
+ if RAW_META_RE =~ header
241
+ cmd = header.slice!(/\A@\w+/) or raise
242
+ @f.ungets(header)
243
+ line "<dt>[UNKNOWN_META_INFO] #{escape_html(cmd)}:</dt>"
244
+ dd_without_p
245
+ next
246
+ end
247
+ case header
248
+ when /\A- \*\*(param|arg)\*\*\s+`([^`]+)`( --(?:.*))?$/m
249
+ line "<dt class='#{@type.to_s}-param'>[PARAM] #{name_html($2 || raise)}:</dt>"
250
+ rest = $3 ? ($3 || raise).sub(/\A --/, '') : +"\n"
251
+ when /\A- \*\*raise\*\*\s+`([^`]+)`( --(?:.*))?$/m
252
+ line "<dt>[EXCEPTION] #{name_html($1 || raise)}:</dt>"
253
+ rest = $2 ? ($2 || raise).sub(/\A --/, '') : +"\n"
254
+ when /\A- \*\*return\*\*( --(?:.*))?$/m
255
+ line "<dt>[RETURN]</dt>"
256
+ rest = $1 ? ($1 || raise).sub(/\A --/, '') : +"\n"
257
+ else
258
+ raise "must not happen: #{header.inspect}"
259
+ end
260
+ # 「@raise Ex 」(説明なし・末尾スペース)は rd では dd 内の空白テキスト行に
261
+ # なるため、rest の空白は潰さずそのまま戻す(+"\n" は凍結回避)
262
+ @f.ungets(rest)
263
+ dd_without_p
264
+ end
265
+ line '</dl>'
266
+ end
267
+
268
+ # - **SEE** [m:X], [m:Y](継続行あり)
269
+ def see
270
+ header = @f.gets or raise
271
+ header = header.sub(SEE_RE, '')
272
+ body = [header] + @f.span(/\A\s+\S/)
273
+ line '<p>'
274
+ line '[SEE_ALSO] ' + compile_text(text_node_from_lines(body))
275
+ line '</p>'
276
+ end
277
+
278
+ # - **`term`**: / - **term**: の定義リスト(説明はインデント行)
279
+ def dlist
280
+ line '<dl>'
281
+ while @f.next? and DLIST_RE =~ @f.peek
282
+ @f.while_match(DLIST_RE) do |l|
283
+ l =~ DLIST_RE or raise
284
+ # rd の dt は term を strip する(term 末尾スペースは dt に含めない)。
285
+ # GFM モード: `term` のコードスパンは <code> として描画し、
286
+ # 中身の参照はその中で解決する(<code><a>...</a></code>。spec/eval)
287
+ term = ($1 || raise)
288
+ id = $2 # {#id} があれば dt にアンカーを付ける(term の再マッチ前に退避)
289
+ if gfm? && term =~ /\A`(.+)`\z/
290
+ inner = ($1 || raise).strip
291
+ line dt("<code>#{rd_compile_text(MarkdownToRRD.restore_inline(inner))}</code>", id)
292
+ else
293
+ term = strip_code_span(term).strip
294
+ line dt(compile_text(term), id)
295
+ end
296
+ inline = l.sub(DLIST_RE, '').strip
297
+ @f.ungets(" #{inline}\n") unless inline.empty?
298
+ end
299
+ dd_with_p
300
+ end
301
+ line '</dl>'
302
+ end
303
+
304
+ # 行頭の空白の桁数(タブも1文字として数える。MD_ITEM_RE のネスト
305
+ # レベル判定と同じ単純な文字数カウント。CommonMark 本来のタブ展開は
306
+ # 行わない — このコンパイラの他の桁数判定と揃えるため)
307
+ def indent_width(line)
308
+ (line[/\A[ \t]*/] || '').size
309
+ end
310
+
311
+ # 空行(1行以上)の直後を先読みし、リスト項目・dd の内容読み取りを
312
+ # どう終えるか判定する。CommonMark のリスト項目/dd は、空行を挟んだ
313
+ # 継続ブロックがマーカーの内容カラム幅以上インデントされていなければ
314
+ # 項目に属さない(spec 0.31.2 Example 255-258・262)。
315
+ #
316
+ # 戻り値:
317
+ # :continue … 空行の後も width 桁以上インデントされた内容が続く。
318
+ # このブロックの一部として続く(空行は消費する)
319
+ # :boundary … 空行の後は boundary_pred が真になる行(次の dt・次の
320
+ # 項目マーカーなど、外側のループがそのまま続けられる
321
+ # 区切り)。このブロックの内容としては終わるが、空行は
322
+ # 消費する(そうしないと外側のループが空行で止まってしまい、
323
+ # 本来1つの <dl>/<ul> であるべきものが分裂する)
324
+ # :stop … どちらでもない。空行は消費しない(トップレベルの段落へ
325
+ # 「逃がす」。spec 0.31.2 Example 255)
326
+ def blank_run_lookahead(width)
327
+ blanks = [] #: Array[String]
328
+ blanks << (@f.gets || raise) while @f.peek&.strip&.empty?
329
+ next_line = @f.peek
330
+ if next_line && indent_width(next_line) >= width
331
+ :continue
332
+ elsif next_line && yield(next_line)
333
+ :boundary
334
+ else
335
+ blanks.reverse_each { |b| @f.ungets(b) }
336
+ :stop
337
+ end
338
+ end
339
+
340
+ # item_list の空行境界判定用: 次の項目マーカー行か(dlist の dt や
341
+ # SEE/param 等、別ディスパッチ経路の行は除く。real_item_marker? と
342
+ # 同じ除外基準)
343
+ def item_marker_line?(line)
344
+ !!(MD_ITEM_RE =~ line && !(DLIST_RE =~ line) && !(SEE_RE =~ line) && !(INFO_RE =~ line))
345
+ end
346
+
347
+ # リスト外・フェンス外で 4 桁以上インデントされたブロックは CommonMark
348
+ # ではインデントコードブロックになるが、このコンパイラは非対応
349
+ # (言語指定できるフェンスへの書き換えを推奨するため。spec 0.31.2
350
+ # Example 264)。互換性を壊さないよう描画は変更せず、検知したら
351
+ # stderr に警告のみ出す(#3232)。将来エラー化する場合は
352
+ # stop_on_syntax_error? に倣ったオプションを追加する
353
+ def warn_if_indented_code_block(line)
354
+ return if indent_width(line) < INDENTED_CODE_BLOCK_WIDTH
355
+ loc = line.location || "#{@f.name}:#{@f.lineno}"
356
+ $stderr.puts "#{loc}: warning: 4+ spaces indented block is not " \
357
+ "rendered as a code block (CommonMark would treat this as an " \
358
+ "indented code block; use a fenced ```lang block instead): " \
359
+ "#{line.strip.inspect}"
360
+ end
361
+
362
+ def strip_code_span(text)
363
+ text.sub(/\A`(.+)`\z/, '\1')
364
+ end
365
+
366
+ # param 名・raise 例外名: GFM モードでは <code> で包む
367
+ def name_html(name)
368
+ gfm? ? "<code>#{escape_html(name)}</code>" : escape_html(name)
369
+ end
370
+
371
+ # GFM テーブルの区切り行(|---|:--:|... 。少なくとも1つの - が必要)
372
+ TABLE_DELIM_RE = /\A\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$/
373
+
374
+ # GFM テーブル。厳格判定: ヘッダ行の直後に区切り行がある場合のみ
375
+ # テーブルとして描画する(本文が | で始まる散文と誤認しない。FalseClass)。
376
+ # テーブルでなければ何も消費せず false を返す
377
+ def try_table
378
+ return false unless gfm?
379
+ header = @f.gets or return false
380
+ unless @f.peek && TABLE_DELIM_RE =~ @f.peek
381
+ @f.ungets(header)
382
+ return false
383
+ end
384
+ aligns = parse_table_aligns(@f.gets || raise)
385
+ line '<table>'
386
+ line '<thead>'
387
+ table_row(header, 'th', aligns)
388
+ line '</thead>'
389
+ line '<tbody>'
390
+ while @f.peek =~ /\A\s*\|/
391
+ table_row(@f.gets || raise, 'td', aligns)
392
+ end
393
+ line '</tbody>'
394
+ line '</table>'
395
+ true
396
+ end
397
+
398
+ def parse_table_aligns(delim)
399
+ split_table_row(delim).map { |cell|
400
+ case cell
401
+ when /\A:-+:\z/ then 'center'
402
+ when /\A:-+\z/ then 'left'
403
+ when /\A-+:\z/ then 'right'
404
+ end
405
+ }
406
+ end
407
+
408
+ def table_row(row, tag, aligns)
409
+ cells = split_table_row(row)
410
+ string '<tr>'
411
+ cells.each_with_index do |cell, i|
412
+ align = aligns[i] ? %Q( align="#{aligns[i]}") : ''
413
+ string "<#{tag}#{align}>#{compile_text(cell)}</#{tag}>"
414
+ end
415
+ line '</tr>'
416
+ end
417
+
418
+ # 行をセルに分割(先頭/末尾の | を除去、\| はリテラルの |)
419
+ def split_table_row(row)
420
+ row.strip.sub(/\A\|/, '').sub(/\|\z/, '')
421
+ .gsub('\\|', "\x03").split('|', -1)
422
+ .map { |c| c.gsub("\x03", '|').strip }
423
+ end
424
+
425
+ # フェンスドコードブロック:
426
+ # - ```lang title="cap"(emlist/samplecode 由来)→ ハイライト付き <pre>
427
+ # - ```(3個・lang なし。//emlist{ 由来)→ 素の <pre>(rstrip escape)
428
+ # - ````+(4個以上。インデントテキスト由来)→ 素の <pre>(rd の list 相当)
429
+ def code_fence
430
+ open_line = @f.gets or raise
431
+ fence = open_line[/\A`+/] or raise
432
+ lang, caption, invalid = parse_fence_info(open_line[fence.size..].to_s.strip)
433
+ terminator = /\A`{#{fence.size}}\s*$/
434
+ if lang
435
+ highlighted_fence_body(lang, caption, terminator, invalid: invalid)
436
+ elsif fence.size == 3
437
+ line '<pre>'
438
+ @f.until_terminator(terminator) do |code_line|
439
+ line escape_html(code_line.rstrip)
440
+ end
441
+ line '</pre>'
442
+ else
443
+ # 変換器はベースインデント(= フェンス長 - 3)を除去して格納している。
444
+ # RDCompiler の list は detab(タブをカラム位置で空白展開)を元の
445
+ # カラムで行うため、ベースインデントを戻してから同じ処理を通す。
446
+ # #@since/#@else ゲートで分断されたインデントブロックは前処理後に
447
+ # 隣接フェンスとして現れるため、続けてマージする(rd では連続
448
+ # インデント行として1つの <pre> になる。ArgumentError 等)
449
+ segments = [[fence.size - 3, collect_fence_lines(terminator)]] #: Array[[Integer, Array[String]]]
450
+ loop do
451
+ # rd の pre は空白行を跨ぐ(list は /\A\S/ まで継続)ため、
452
+ # フェンス間の空白のみ行も次が 4+ フェンスならブロックの一部
453
+ blanks = [] #: Array[String]
454
+ blanks << (@f.gets || raise) while @f.peek =~ /\A\s*$/
455
+ unless @f.peek =~ /\A(`{4,})\s*$/
456
+ blanks.reverse_each { |b| @f.ungets(b) }
457
+ break
458
+ end
459
+ next_fence = ($1 || raise)
460
+ @f.gets
461
+ segments.last[1].concat(blanks)
462
+ segments << [next_fence.size - 3,
463
+ collect_fence_lines(/\A`{#{next_fence.size}}\s*$/)]
464
+ end
465
+ lines = segments.flat_map { |base, ls|
466
+ ls.map { |l| l =~ /\A\s*\z/ ? l : (' ' * base) + l }
467
+ }
468
+ lines = unindent_block(canonicalize(lines))
469
+ while lines.last&.empty?
470
+ lines.pop
471
+ end
472
+ line '<pre>'
473
+ lines.each do |code_line|
474
+ line escape_html(code_line)
475
+ end
476
+ line '</pre>'
477
+ end
478
+ end
479
+
480
+ def collect_fence_lines(terminator)
481
+ lines = [] #: Array[String]
482
+ @f.until_terminator(terminator) do |code_line|
483
+ lines << code_line
484
+ end
485
+ lines
486
+ end
487
+
488
+ # info string のパース。文法: lang [invalid] [title="cap"]
489
+ # invalid は「構文として完全でないコード」の印で、ruby では Ripper の
490
+ # 構文チェックをせず Rouge の lexer で色付けする(issue #251)
491
+ def parse_fence_info(rest)
492
+ if rest =~ /\A(\w+)?(\s+invalid)?(?:\s+title="((?:[^"\\]|\\.)*)")?\z/
493
+ # title の gsub(別の正規表現マッチ)が $~ を上書きするため、
494
+ # $1〜$3 は gsub より前に読み切る
495
+ lang = $1
496
+ invalid = !$2.nil?
497
+ title = $3
498
+ [lang, title&.gsub(/\\(["\\])/, '\1'), invalid]
499
+ else
500
+ [nil, nil, false]
501
+ end
502
+ end
503
+
504
+ # ハイライト付き <pre> の本体。caption はタブとして pre の前に置く
505
+ # (rd 側と同期)。strip_re はインデントフェンスのデデント用
506
+ def highlighted_fence_body(lang, caption, terminator, strip_re = nil, invalid: false)
507
+ line "<span class=\"caption\">#{escape_html(caption)}</span>" if caption
508
+ # <code> の直後に改行を入れると pre の内容の先頭に余計な空行として
509
+ # 表示されるため、コード本体は <code> に直接続ける(#254、rd 側と同期)
510
+ string "<pre class=\"highlight #{lang}\"><code>"
511
+ src = +""
512
+ @f.until_terminator(terminator) do |code_line|
513
+ src << (strip_re ? code_line.sub(strip_re, '') : code_line)
514
+ end
515
+ string highlight_source(src, lang, caption, invalid: invalid)
516
+ line '</code></pre>'
517
+ end
518
+
519
+ # リスト項目・dlist 説明の中のインデントされたフェンス(GFM のリスト内
520
+ # コードブロック)。GFM と同じく、内容と閉じフェンスからフェンス行の
521
+ # インデント幅までを除去する
522
+ def indented_code_fence
523
+ open_line = @f.gets or raise
524
+ INDENTED_FENCE_RE =~ open_line or raise
525
+ indent = ($1 || raise)
526
+ fence = ($2 || raise)
527
+ lang, caption, invalid = parse_fence_info(open_line[(indent.size + fence.size)..].to_s.strip)
528
+ terminator = /\A[ \t]{0,#{indent.size}}`{#{fence.size}}\s*$/
529
+ strip_re = /\A[ \t]{1,#{indent.size}}/
530
+ if lang
531
+ highlighted_fence_body(lang, caption, terminator, strip_re, invalid: invalid)
532
+ else
533
+ line '<pre>'
534
+ @f.until_terminator(terminator) do |code_line|
535
+ line escape_html(code_line.sub(strip_re, '').rstrip)
536
+ end
537
+ line '</pre>'
538
+ end
539
+ end
540
+
541
+ # 箇条書き(- item / N. item、ネスト・継続行あり)
542
+ def item_list(level = 0)
543
+ open_tag = nil
544
+ close_tag = nil
545
+ case @f.peek
546
+ when /\A\s*- /
547
+ open_tag = "<ul>"
548
+ close_tag = "</ul>"
549
+ when /\A\s*\d+\. /
550
+ open_tag = "<ol>"
551
+ close_tag = "</ol>"
552
+ end
553
+ line open_tag
554
+ @item_stack.push(close_tag)
555
+ @f.while_match(MD_ITEM_RE) do |item_line|
556
+ string "<li>"
557
+ @item_stack.push("</li>")
558
+ string compile_text(item_line.sub(/\A\s*(?:-|\d+\.)\s?/, '').strip)
559
+ item_line =~ MD_ITEM_RE or raise
560
+ # この項目の内容カラム幅(マーカー前の空白 + マーカー自身。
561
+ # "- " なら2、"12. " なら4)。空行を挟んだ継続がこの項目に
562
+ # 属するかどうかの判定に使う
563
+ content_width = ($1 || raise).size + ($2 || raise).size
564
+ # 継続行(折り返しテキスト)とインデントフェンス(項目内コードブロック)。
565
+ # CommonMark に合わせ、空行を挟まない直接継続(lazy continuation)は
566
+ # インデント幅を問わず許容するが、フェンスや空行を挟んだ継続は
567
+ # content_width 以上のインデントが無ければ項目に属さない
568
+ # (spec 0.31.2 Example 255-258・262-263、doctree#3232 znz レビュー。
569
+ # 従来はここで空行に当たると即座に項目の外へ「脱走」していた)
570
+ while @f.peek
571
+ if @f.peek&.strip&.empty?
572
+ if blank_run_lookahead(content_width) { |l| item_marker_line?(l) } == :continue
573
+ next
574
+ else
575
+ break
576
+ end
577
+ elsif INDENTED_FENCE_RE =~ @f.peek
578
+ break if indent_width(@f.peek || raise) < content_width
579
+ nl
580
+ indented_code_fence
581
+ elsif /\A\s+(?!- |\d+\. )\S/ =~ @f.peek
582
+ nl
583
+ string compile_text((@f.gets || raise).strip)
584
+ else
585
+ break
586
+ end
587
+ end
588
+ if (m = MD_ITEM_RE.match(@f.peek)) && level < (m[1] || raise).size
589
+ item_list((m[1] || raise).size)
590
+ line @item_stack.pop # current level li
591
+ break if MD_ITEM_RE =~ @f.peek and level > ($1 || raise).size
592
+ elsif m && level > (m[1] || raise).size
593
+ line @item_stack.pop # current level li
594
+ break
595
+ else
596
+ line @item_stack.pop # current level li
597
+ end
598
+ end
599
+ line @item_stack.pop unless @item_stack.empty?
600
+ end
601
+
602
+ # RDCompiler の dd_with_p / dd_without_p の md 版:
603
+ # コードブロック(rd では //emlist)の判定をインデントフェンスに差し替える。
604
+ # RD の //emlist は桁0で書いても dd に取り込まれたが、Markdown では
605
+ # CommonMark に合わせて「項目の内容カラムにインデントされたフェンス」だけを
606
+ # 説明(dd)の一部として取り込む。桁0のフェンスは dd の外(トップレベル)。
607
+ #
608
+ # CommonMark に合わせ、空行を挟まない直接継続(lazy continuation)は
609
+ # インデント幅を問わず許容するが、フェンスや空行を挟んだ継続は
610
+ # DLIST_CONTENT_WIDTH(2) 以上のインデントが無ければ dd に属さない
611
+ # (spec 0.31.2 Example 255-258、doctree#3232 znz レビュー。symref.md
612
+ # の「半角スペース1個だけの継続」はこのため dd の外の段落になる)
613
+ def dd_with_p
614
+ line '<dd>'
615
+ while @f.peek
616
+ if @f.peek&.strip&.empty?
617
+ if blank_run_lookahead(DLIST_CONTENT_WIDTH) { |l| !!(DLIST_RE =~ l) } == :continue
618
+ next
619
+ else
620
+ break
621
+ end
622
+ elsif INDENTED_FENCE_RE =~ @f.peek
623
+ break if indent_width(@f.peek || raise) < DLIST_CONTENT_WIDTH
624
+ indented_code_fence
625
+ elsif /\A[ \t]/ =~ @f.peek
626
+ line '<p>'
627
+ line compile_text(text_node_from_lines(@f.span(DD_TEXT_RE)))
628
+ line '</p>'
629
+ else
630
+ break
631
+ end
632
+ end
633
+ line '</dd>'
634
+ end
635
+
636
+ def dd_without_p
637
+ line '<dd>'
638
+ while /\A[ \t]/ =~ @f.peek
639
+ case @f.peek
640
+ when INDENTED_FENCE_RE
641
+ indented_code_fence
642
+ when /\A[ \t]/
643
+ line compile_text(text_node_from_lines(@f.span(DD_TEXT_RE)))
644
+ end
645
+ end
646
+ line '</dd>'
647
+ end
648
+
649
+ # テキストノード: インライン記法を rd 形式へ復元してから既存の
650
+ # コンパイル(エスケープ・リンク解決)を継承する。
651
+ # M1 等価モード: 変換器が付けた __X__ の自動コードスパンは剥がす
652
+ def compile_text(str)
653
+ return compile_gfm_text(str) if gfm?
654
+ super(restore_rd_text(str))
655
+ end
656
+
657
+ # GFM モードの地の文 `**strong**`。保守的サブセット: 同一テキスト
658
+ # ノード内で閉じる(段落は行結合済みなので GFM と同じく soft break は
659
+ # またげる)・
660
+ # 開閉の内側が空白でない・内容に `*` を含まない(入れ子なし)・
661
+ # 単語内には適用しない(`2**32` のような冪乗演算子を巻き込まないため。
662
+ # 開きの直前と閉じの直後が英数字・`_`・`*` なら対象外)。
663
+ # `*em*`/`_em_` は多義性が高いため対象外(bitclust#301)
664
+ INLINE_STRONG_RE = /(?<![0-9A-Za-z_*])\*\*(?=\S)([^*\n]+?)(?<=\S)\*\*(?![0-9A-Za-z_*])/
665
+
666
+ # GFM モードのテキストコンパイル:
667
+ # コードスパン(CommonMark 6.1、N 連バッククォート)→ <code>(中身は
668
+ # 参照解決しない・HTML エスケープのみ)、行頭 **N.** → <strong>、
669
+ # Markdown リンク(<url> 自動リンク・[テキスト](URL)・
670
+ # [テキスト](#アンカー))→ <a>。
671
+ # コードスパンの抽出はリンク抽出より先に行う(CommonMark のインライン
672
+ # 優先順位どおり。コードスパン内は他の記法を解決しない・
673
+ # extract_md_links 側で改めて分割する必要がない)。
674
+ # エスケープ済み \` はコードスパンの外だけリテラルのバッククォートに
675
+ # 復元される(restore_inline の convert_bare_refs が \[ \] \` の復元を
676
+ # 担う。コードスパン内ではバックスラッシュエスケープが無効という
677
+ # CommonMark のルールどおり、中身は素通しでバックスラッシュごと残す)
678
+ def compile_gfm_text(str)
679
+ code_spans = [] #: Array[String]
680
+ str = extract_code_spans(str, code_spans)
681
+ links = [] #: Array[String]
682
+ str = extract_md_links(str, links)
683
+ str = str.gsub(/^\*\*(\d+\.)\*\* /, "\x01\\1\x02 ")
684
+ str = str.gsub(INLINE_STRONG_RE, "\x01\\1\x02")
685
+ rd_compile_text(MarkdownToRRD.restore_inline(str))
686
+ .gsub("\x01", '<strong>').gsub("\x02", '</strong>')
687
+ .gsub(/\x03(\d+)\x03/) { links[($1 || raise).to_i] || raise }
688
+ .gsub(/\x04(\d+)\x04/) { code_spans[($1 || raise).to_i] || raise }
689
+ end
690
+
691
+ # CommonMark 6.1 のインラインコードスパンを描画済み <code> へ退避し
692
+ # \x04idx\x04 プレースホルダに置き換える(extract_md_links と同じ流儀)。
693
+ # 開始と同じ長さのバッククォート列で閉じる(最長一致ではなく同長
694
+ # ペアリング)。閉じる相手が見つからない開始列はコードスパンにせず、
695
+ # そのまま次の候補から探索を続ける(非対称の開始列=リテラル)。
696
+ # 改行はまたがない(テキストノードの行区切りを保持する既存の制約を
697
+ # 維持する)。
698
+ # \` は開始候補にしない(CommonMark: バックスラッシュエスケープされた
699
+ # バッククォートはコードスパンを開始しない。2.4 の \`not code\` 例)。
700
+ # 閉じ側の探索はエスケープを見ない(コードスパン内ではバックスラッシュ
701
+ # エスケープが無効なため、開いた後は同じ長さのバッククォート列が
702
+ # 来れば必ず閉じる。`foo\`bar` → <code>foo\</code>bar` という
703
+ # GitHub の実描画と同じ)
704
+ def extract_code_spans(str, saved)
705
+ result = +''
706
+ i = 0
707
+ len = str.length
708
+ while i < len
709
+ c = str[i] or raise
710
+ if c == '`' && (i.zero? || str[i - 1] != '\\')
711
+ run_len = 1
712
+ run_len += 1 while str[i + run_len] == '`'
713
+ close = find_code_span_close(str, i + run_len, run_len)
714
+ if close
715
+ content = str[(i + run_len)...close] || raise
716
+ saved << "<code>#{escape_html(normalize_code_span(content))}</code>"
717
+ result << "\x04#{saved.size - 1}\x04"
718
+ i = close + run_len
719
+ next
720
+ else
721
+ result << ('`' * run_len)
722
+ i += run_len
723
+ next
724
+ end
725
+ end
726
+ result << c
727
+ i += 1
728
+ end
729
+ result
730
+ end
731
+
732
+ # from 位置以降で、run_len と同じ長さのバッククォート列(開始位置)を
733
+ # 探す。改行をまたいだら諦める(見つからないのと同じ扱い)
734
+ def find_code_span_close(str, from, run_len)
735
+ i = from
736
+ len = str.length
737
+ while i < len
738
+ c = str[i] or raise
739
+ case c
740
+ when "\n"
741
+ return nil
742
+ when '`'
743
+ j = i
744
+ j += 1 while str[j] == '`'
745
+ return i if j - i == run_len
746
+ i = j
747
+ else
748
+ i += 1
749
+ end
750
+ end
751
+ nil
752
+ end
753
+
754
+ # CommonMark 6.1: 前後が両方スペースで、かつ内容が全部スペースでは
755
+ # ない場合に前後を1個ずつ剥ぐ(`` `a` `` の内容を `a` として書ける
756
+ # ようにするための規則)
757
+ def normalize_code_span(content)
758
+ if content.start_with?(' ') && content.end_with?(' ') && content.match?(/[^ ]/)
759
+ content[1..-2] || ''
760
+ else
761
+ content
762
+ end
763
+ end
764
+
765
+ AUTOLINK_RE = %r{<(https?://[^<>\s]+)>}
766
+ # インラインリンクの宛先として受ける形。散文の「[c:String](を参照)」の
767
+ # ような括弧書きをリンクと誤認しないため URL と #フラグメントに限る
768
+ # (MARKUP_SPEC §7.4/§7.5)
769
+ # リンク宛先として受け付けるもの: 外部 URL・ページ内アンカー・
770
+ # rurema 参照 scheme(c:/m:/lib:/d:/f:。ラベル付きで参照したいとき用)
771
+ MD_LINK_DEST_RE = %r{\A(?:https?://|\#|(?:c|m|lib|d|f|ref|man):)}
772
+
773
+ # Markdown のリンクを描画済み <a> へ退避し \x03idx\x03 プレースホルダに
774
+ # 置き換える。後段の rd_compile_text(HTML エスケープ・参照解決)を
775
+ # 素通しし、compile_gfm_text の最後で戻す
776
+ def extract_md_links(str, saved)
777
+ str = str.gsub(AUTOLINK_RE) {
778
+ saved << direct_url($1 || raise)
779
+ "\x03#{saved.size - 1}\x03"
780
+ }
781
+ return str unless str.include?('[')
782
+ result = +''
783
+ i = 0
784
+ while i < str.length
785
+ if str[i] == '\\' && i + 1 < str.length
786
+ result << (str[i, 2] || raise)
787
+ i += 2
788
+ next
789
+ end
790
+ if str[i] == '['
791
+ close = matching_delimiter(str, i, '[', ']')
792
+ if close && str[close + 1] == '(' &&
793
+ (dest_end = matching_delimiter(str, close + 1, '(', ')', space_ends: true)) &&
794
+ (dest = str[(close + 2)...dest_end]) && MD_LINK_DEST_RE =~ dest
795
+ saved << md_link(str[(i + 1)...close] || raise, dest)
796
+ result << "\x03#{saved.size - 1}\x03"
797
+ i = dest_end + 1
798
+ next
799
+ end
800
+ end
801
+ result << (str[i] || raise)
802
+ i += 1
803
+ end
804
+ result
805
+ end
806
+
807
+ # open 位置の括弧に対応する閉じ括弧の位置(\ エスケープ対応・ネスト可)。
808
+ # space_ends: リンク宛先用。空白が現れたらリンクではない(nil)
809
+ def matching_delimiter(str, open, open_char, close_char, space_ends: false)
810
+ depth = 0
811
+ i = open
812
+ while i < str.length
813
+ c = str[i]
814
+ if c == '\\'
815
+ i += 1
816
+ elsif space_ends && c =~ /\s/
817
+ return nil
818
+ elsif c == open_char
819
+ depth += 1
820
+ elsif c == close_char
821
+ depth -= 1
822
+ return i if depth == 0
823
+ end
824
+ i += 1
825
+ end
826
+ nil
827
+ end
828
+
829
+ # 表示テキスト付きリンクの描画。テキストは参照解決しないプレーン表示
830
+ # (リンク内リンクは HTML として成立しないため)。外部 URL は rd の
831
+ # [[url:]] と同じ external クラス、#アンカーはページ内リンク。
832
+ # 宛先が rurema 参照 scheme(c:/m:/lib:/d:/f:)のときは内部リンクとして
833
+ # 解決する([`egd_bytes(filename, 255)`](m:OpenSSL::Random?.egd_bytes)
834
+ # のように、呼び出し形をラベルにしてメソッドへリンクできる)
835
+ def md_link(text, dest)
836
+ if (m = /\A(c|m|lib|d|f|ref|man):(.+)\z/m.match(dest))
837
+ return ref_md_link(m[1] || raise, m[2] || raise, text)
838
+ end
839
+ label = escape_html(unescape_md_brackets(text))
840
+ href = escape_html(unescape_md_brackets(dest))
841
+ if dest.start_with?('#')
842
+ %Q(<a href="#{href}">#{label}</a>)
843
+ else
844
+ %Q(<a class="external" href="#{href}">#{label}</a>)
845
+ end
846
+ end
847
+
848
+ # 参照 scheme 宛先のリンク。解決・compileerror 表示・link_checker への
849
+ # 通知は bracket_link にそのまま乗せ、ラベルだけ後から差し替える
850
+ # (a_href がラベルを HTML エスケープするため、コードスパンを含む
851
+ # ラベル HTML はプレースホルダ経由で入れる)
852
+ MD_REF_LABEL_PLACEHOLDER = "\x05L\x05"
853
+ def ref_md_link(type, arg, text)
854
+ arg = unescape_md_brackets(arg)
855
+ # md ソースの module function 表記 ?. を内部表記 .# へ正規化する
856
+ # (単一ブラケット参照の restore_inline と同じ規則。bitclust#282)
857
+ arg = arg.sub('?.', '.#') if type == 'm'
858
+ html = bracket_link("#{type}:#{arg}", MD_REF_LABEL_PLACEHOLDER)
859
+ html.gsub(MD_REF_LABEL_PLACEHOLDER, md_ref_link_label(text))
860
+ end
861
+
862
+ # ラベルの描画: 全体がコードスパン(`...`)ならば <code>、それ以外は
863
+ # プレーン表示。ラベル内のその他のインライン記法は解釈しない
864
+ def md_ref_link_label(text)
865
+ text = unescape_md_brackets(text)
866
+ if (m = /\A`(.+)`\z/m.match(text))
867
+ "<code>#{escape_html(m[1] || raise)}</code>"
868
+ else
869
+ escape_html(text)
870
+ end
871
+ end
872
+
873
+ def unescape_md_brackets(str)
874
+ str.gsub(/\\([\[\]\\])/, '\1')
875
+ end
876
+
877
+ def restore_rd_text(str)
878
+ # M1 等価モード: md のコードスパンを rd の元表記へ戻して描画する
879
+ MarkdownToRRD.restore_text(str)
880
+ end
881
+ end
882
+
883
+ end