diff --git a/doc/markdown-samples/MARKUP_SPEC.md b/doc/markdown-samples/MARKUP_SPEC.md
index e07eb21..32f6aa0 100644
--- a/doc/markdown-samples/MARKUP_SPEC.md
+++ b/doc/markdown-samples/MARKUP_SPEC.md
@@ -731,6 +731,9 @@ code line 2
リスト項目や定義リスト(§10.2)の説明の中にコードブロックを置く場合は、
GFM と同じくフェンスを項目のインデントに合わせて書く。
内容と閉じフェンスからはフェンス行のインデント幅までが除去される。
+CommonMark と同じく、項目の説明に取り込まれるのはインデントされた
+フェンスだけで、カラム0(インデントなし)のフェンスは項目の外の
+トップレベルのコードブロックとして扱われる。
````markdown
- **`Dir.glob`**:
diff --git a/lib/bitclust/markdown_to_rrd.rb b/lib/bitclust/markdown_to_rrd.rb
index 60196d3..3b4ce05 100644
--- a/lib/bitclust/markdown_to_rrd.rb
+++ b/lib/bitclust/markdown_to_rrd.rb
@@ -365,6 +365,53 @@ def convert_code_block(line)
end
end
+ # dd/@param 系の説明内にある、インデントされたフェンス(rd では
+ # //emlist・#@samplecode の桁0出力に対応。CommonMark 準拠のため
+ # rrd_to_markdown.rb が項目の内容カラムへインデントして出すように
+ # なった分の桁0版)を、桁0の //emlist{ / #@samplecode に戻す。
+ # convert_code_block の 3 バッククォート(lang 付き)分岐と対称。
+ # 4+ バッククォート(インデントコード由来)のインデント版は
+ # rrd_to_markdown.rb が dd/@param からその形式を出さないため未対応
+ def convert_indented_code_block(line, indent)
+ line =~ /\A[ \t]+(`{3,})(\w*)(?:\s+title="((?:[^"\\]|\\.)*)")?\s*$/ or raise
+ fence_len = ($1 || raise).length
+ lang = ($2 || raise).empty? ? nil : $2
+ title = $3&.gsub(/\\(["\\])/, '\1')
+ strip_re = /\A[ \t]{1,#{indent.length}}/
+
+ if lang == 'ruby'
+ samplecode = '#@samplecode'
+ samplecode_end = '#@end'
+ @out << (title && !title.empty? ? "#{samplecode} #{title}\n" : "#{samplecode}\n")
+ else
+ parts = ['//emlist']
+ if title && !title.empty?
+ parts << "[#{title}]"
+ parts << "[#{lang}]" if lang
+ elsif lang
+ parts << '[]'
+ parts << "[#{lang}]"
+ end
+ parts << "{"
+ @out << parts.join + "\n"
+ end
+ advance
+ terminator = /\A[ \t]{0,#{indent.length}}`{#{fence_len}}\s*\n?\z/
+ while @index < @lines.length
+ l = current_line
+ if l =~ terminator
+ @out << (lang == 'ruby' ? "#{samplecode_end}\n" : "//}\n")
+ advance
+ return
+ elsif l =~ /\A\#@/
+ @out << l # ディレクティブはカラム0のまま(rrd_to_markdown.rb 側と対称)
+ else
+ @out << l.sub(strip_re, '')
+ end
+ advance
+ end
+ end
+
def convert_method_signature(line)
@out << line.sub(/\A### def /, '--- ')
advance
@@ -419,7 +466,7 @@ def collect_md_continuation_lines(greedy: false)
# rd 側と対称: nest 0 の #@end も透過(fileutils の @param 継続)
nest -= 1 if nest > 0
raw_passthrough(line)
- elsif line =~ /\A\s+\S/ && line !~ /\A- / && line !~ /\A\*\*/ && line !~ /\A```/
+ elsif line =~ /\A\s+\S/ && line !~ /\A- / && line !~ /\A\*\*/ && line !~ /\A[ \t]*`{3,}/
@out << convert_inline_refs(line)
advance
elsif greedy && line =~ /\A[ \t]+$/
@@ -427,6 +474,8 @@ def collect_md_continuation_lines(greedy: false)
advance
elsif greedy && line =~ /\A`{3,}/
convert_code_block(line) # rd 側と対称: 直結するコードブロックは説明の一部
+ elsif greedy && line =~ /\A([ \t]+)`{3,}/
+ convert_indented_code_block(line, $1 || raise) # 上と同じくインデント版
else
break
end
@@ -515,11 +564,13 @@ def convert_dlist_colon_item(line)
# 交互に何個でも説明として受ける(String#% 型)
while @index < @lines.length
l = current_line
- if l =~ /\A\s+\S/
+ if l =~ /\A\s+\S/ && l !~ /\A[ \t]+`{3,}/
@out << convert_inline_refs(l)
advance
elsif l =~ /\A`{3,}/
convert_code_block(l)
+ elsif l =~ /\A([ \t]+)`{3,}/
+ convert_indented_code_block(l, $1 || raise) # 上と同じくインデント版
elsif l =~ /\A\#@/
# rd 側と対称: #@ 指令行は dd の文脈に透明
raw_passthrough(l)
diff --git a/lib/bitclust/mdcompiler.rb b/lib/bitclust/mdcompiler.rb
index 4cec201..4fa2b63 100644
--- a/lib/bitclust/mdcompiler.rb
+++ b/lib/bitclust/mdcompiler.rb
@@ -502,10 +502,13 @@ def item_list(level = 0)
end
# RDCompiler の dd_with_p / dd_without_p の md 版:
- # コードブロック(rd では //emlist)の判定をフェンスに差し替える
+ # コードブロック(rd では //emlist)の判定をインデントフェンスに差し替える。
+ # RD の //emlist は桁0で書いても dd に取り込まれたが、Markdown では
+ # CommonMark に合わせて「項目の内容カラムにインデントされたフェンス」だけを
+ # 説明(dd)の一部として取り込む。桁0のフェンスは dd の外(トップレベル)。
def dd_with_p
line '
'
- while /\A(?:\s|\z)/ =~ @f.peek or FENCE_RE =~ @f.peek
+ while /\A(?:\s|\z)/ =~ @f.peek
case @f.peek
when /\A$/
@f.gets
@@ -515,8 +518,6 @@ def dd_with_p
line ''
line compile_text(text_node_from_lines(@f.span(DD_TEXT_RE)))
line '
'
- when FENCE_RE
- code_fence
else
raise 'must not happen'
end
@@ -526,14 +527,12 @@ def dd_with_p
def dd_without_p
line ''
- while /\A[ \t]/ =~ @f.peek or FENCE_RE =~ @f.peek
+ while /\A[ \t]/ =~ @f.peek
case @f.peek
when INDENTED_FENCE_RE
indented_code_fence
when /\A[ \t]/
line compile_text(text_node_from_lines(@f.span(DD_TEXT_RE)))
- when FENCE_RE
- code_fence
end
end
line ''
diff --git a/lib/bitclust/rrd_to_markdown.rb b/lib/bitclust/rrd_to_markdown.rb
index 8f0af9e..7f15228 100644
--- a/lib/bitclust/rrd_to_markdown.rb
+++ b/lib/bitclust/rrd_to_markdown.rb
@@ -356,7 +356,28 @@ def process_body
end
end
- def convert_emlist(line)
+ # dd(記述リストの説明)/ @param 等の説明の中でフェンスを出すときの
+ # 内容カラムのインデント幅。項目マーカー「- 」の幅に合わせる
+ DD_CONTENT_INDENT = ' '
+
+ # フェンス内容行にインデントを前置する。ただし:
+ # - 空行には前置しない(末尾空白を増やさないため)
+ # - 前処理ディレクティブ行(#@since/#@until/#@if/#@else/#@end 等)には
+ # 前置しない。Preprocessor はこれらを行頭カラム0でしか認識しない
+ # (bitclust/lib/bitclust/preprocessor.rb の /\A\#@since\b/ 等)ため、
+ # インデントすると版解決が効かなくなる
+ def indent_content_line(line, indent)
+ return line if indent.empty?
+ return line if line =~ /\A[ \t]*\n?\z/
+ return line if line =~ /\A\#@/
+ indent + line
+ end
+
+ # indent: 呼び出し文脈の内容カラム(トップレベルは既定の ''、
+ # dd/@param 系の説明内から呼ぶ場合は DD_CONTENT_INDENT)。
+ # CommonMark でリスト項目/dd の説明に確実にネストさせるための字下げで、
+ # 新しい MDCompiler の INDENTED_FENCE_RE がこれを見て dd/li に取り込む
+ def convert_emlist(line, indent = '')
# //emlist[caption][lang]{...//}
caption = nil
lang = nil
@@ -375,16 +396,16 @@ def convert_emlist(line)
escaped_caption = caption.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
parts << " title=\"#{escaped_caption}\""
end
- @out << parts.join + "\n"
+ @out << indent + parts.join + "\n"
advance
while @index < @lines.length
line = current_line
if line =~ /\A\/\/\}\s*$/
- @out << "```\n"
+ @out << indent + "```\n"
advance
return
end
- @out << line
+ @out << indent_content_line(line, indent)
advance
end
end
@@ -392,13 +413,13 @@ def convert_emlist(line)
DIRECTIVE_NEST_RE = /\A\#@(?:since|until|if)\b/
DIRECTIVE_END_RE = /\A\#@end\s*$/
- def convert_samplecode(line)
+ def convert_samplecode(line, indent = '')
label = line.sub(SAMPLECODE_RE, '').strip
if label.empty?
- @out << "```ruby\n"
+ @out << indent + "```ruby\n"
else
escaped_label = label.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
- @out << "```ruby title=\"#{escaped_label}\"\n"
+ @out << indent + "```ruby title=\"#{escaped_label}\"\n"
end
advance
nest = 0
@@ -406,20 +427,20 @@ def convert_samplecode(line)
line = current_line
if line =~ DIRECTIVE_NEST_RE
nest += 1
- @out << line
+ @out << line # ディレクティブは常にカラム0(Preprocessor 要件)
advance
elsif line =~ DIRECTIVE_END_RE
if nest > 0
nest -= 1
- @out << line
+ @out << line # 同上
advance
else
- @out << "```\n"
+ @out << indent + "```\n"
advance
return
end
else
- @out << line
+ @out << indent_content_line(line, indent)
advance
end
end
@@ -514,9 +535,11 @@ def collect_continuation_lines(greedy: false)
@out << line
advance
elsif greedy && line =~ SAMPLECODE_RE
- convert_samplecode(line) # RDCompiler の dd はコードブロックも説明の一部
+ # RDCompiler の dd はコードブロックも説明の一部。新 MDCompiler が
+ # dd に取り込めるよう、内容カラムにインデントしたフェンスで出す
+ convert_samplecode(line, DD_CONTENT_INDENT)
elsif greedy && line =~ /\A\/\/emlist/
- convert_emlist(line)
+ convert_emlist(line, DD_CONTENT_INDENT)
else
break
end
@@ -627,9 +650,11 @@ def convert_dlist_item(line, term)
@out << convert_inline_refs(l)
advance
elsif l =~ SAMPLECODE_RE
- convert_samplecode(l)
+ # 項目の内容カラムにインデントしたフェンスにする(CommonMark で
+ # dd にネストさせ、新 MDCompiler の INDENTED_FENCE_RE に取り込ませる)
+ convert_samplecode(l, DD_CONTENT_INDENT)
elsif l =~ /\A\/\/emlist/
- convert_emlist(l)
+ convert_emlist(l, DD_CONTENT_INDENT)
elsif l =~ /\A\#@/
# #@ 指令行は dd の文脈に透明(版解決後は消えて説明が連続する。
# ここで打ち切ると後続インデント行がコード扱いになる。pack-template)
@@ -656,7 +681,12 @@ def convert_dlist_item(line, term)
end
end
- def convert_indented_code(line)
+ # indent: 呼び出し文脈の内容カラム(他のフェンス出力メソッドと揃えるため
+ # 用意。現状このメソッドはトップレベル(process_body)からしか呼ばれず、
+ # RDCompiler の list(インデントのみのコード相当)は dd/li には取り込まれ
+ # ないため、呼び出し側は常に既定の '' のまま。将来 dd/li 文脈から呼ぶ
+ # 必要が生じた場合のための口として引数だけ揃えておく
+ def convert_indented_code(line, indent = '')
# 行を収集してベースインデントを取得
code_lines = [] #: Array[String]
while @index < @lines.length
@@ -687,15 +717,15 @@ def convert_indented_code(line)
.min || 1
fence = '`' * (3 + base_indent)
- @out << "#{fence}\n"
+ @out << indent + "#{fence}\n"
code_lines.each do |l|
if l =~ /\A\s*$/
@out << l
else
- @out << (l[base_indent..] || raise) # ベースインデントを除去
+ @out << indent + (l[base_indent..] || raise) # ベースインデントを除去
end
end
- @out << "#{fence}\n"
+ @out << indent + "#{fence}\n"
end
def convert_h1(line)
diff --git a/sig/bitclust/markdown_to_rrd.rbs b/sig/bitclust/markdown_to_rrd.rbs
index b86dc0d..709e769 100644
--- a/sig/bitclust/markdown_to_rrd.rbs
+++ b/sig/bitclust/markdown_to_rrd.rbs
@@ -61,6 +61,12 @@ module BitClust
def convert_code_block: (String line) -> void
+ # dd/@param 系の説明内にある、インデントされたフェンス(rd では
+ # //emlist・#@samplecode の桁0出力に対応)を桁0の //emlist{ /
+ # #@samplecode に戻す。convert_code_block の 3 バッククォート
+ # (lang 付き)分岐と対称
+ def convert_indented_code_block: (String line, String indent) -> void
+
def convert_method_signature: (String line) -> void
def convert_module_function_signature: (String line) -> void
diff --git a/sig/bitclust/rrd_to_markdown.rbs b/sig/bitclust/rrd_to_markdown.rbs
index e9d4f4d..e8677b6 100644
--- a/sig/bitclust/rrd_to_markdown.rbs
+++ b/sig/bitclust/rrd_to_markdown.rbs
@@ -64,13 +64,22 @@ module BitClust
def process_body: () -> void
- def convert_emlist: (String line) -> void
+ # dd(記述リストの説明)/ @param 等の説明の中でフェンスを出すときの
+ # 内容カラムのインデント幅。項目マーカー「- 」の幅に合わせる
+ DD_CONTENT_INDENT: String
+
+ # フェンス内容行にインデントを前置する(空行・前処理ディレクティブ行は除く)
+ def indent_content_line: (String line, String indent) -> String
+
+ # indent: 呼び出し文脈の内容カラム(トップレベルは既定の ''、
+ # dd/@param 系の説明内から呼ぶ場合は DD_CONTENT_INDENT)
+ def convert_emlist: (String line, ?String indent) -> void
DIRECTIVE_NEST_RE: ::Regexp
DIRECTIVE_END_RE: ::Regexp
- def convert_samplecode: (String line) -> void
+ def convert_samplecode: (String line, ?String indent) -> void
def convert_signature: (String line) -> void
@@ -106,7 +115,7 @@ module BitClust
# term は process_body の正規表現キャプチャ($1 = String?)由来
def convert_dlist_item: (String line, untyped term) -> void
- def convert_indented_code: (String line) -> void
+ def convert_indented_code: (String line, ?String indent) -> void
def convert_h1: (String line) -> void
diff --git a/test/test_rrd_to_markdown.rb b/test/test_rrd_to_markdown.rb
index fa2352e..05f2f1c 100644
--- a/test/test_rrd_to_markdown.rb
+++ b/test/test_rrd_to_markdown.rb
@@ -116,10 +116,12 @@ def test_dlist_description_across_directives
def test_dlist_description_across_samplecode
# pack-template 型: dd 内の #@samplecode 後の字下げ行も dd 段落継続。
# 空行スキャンが #@samplecode を透過するとコード本体の行(カラム0)を見て
- # dd を打ち切り、後続段落がトップレベル pre と誤分類される
+ # dd を打ち切り、後続段落がトップレベル pre と誤分類される。
+ # フェンスは CommonMark で dd にネストするよう内容カラム(2桁)へインデント
+ # される(新 MDCompiler の INDENTED_FENCE_RE が取り込む対象)
rrd = ": w\n\n 説明。\n\n\#@samplecode\n[0].pack(\"w\")\n\#@end\n\n なお、後続の説明。\n"
md = convert(rrd)
- assert_equal "- **`w`**:\n\n 説明。\n\n```ruby\n[0].pack(\"w\")\n```\n\n なお、後続の説明。\n", md
+ assert_equal "- **`w`**:\n\n 説明。\n\n ```ruby\n [0].pack(\"w\")\n ```\n\n なお、後続の説明。\n", md
assert_equal rrd, BitClust::MarkdownToRRD.convert(md)
end