Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/markdown-samples/MARKUP_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,9 @@ code line 2
リスト項目や定義リスト(§10.2)の説明の中にコードブロックを置く場合は、
GFM と同じくフェンスを項目のインデントに合わせて書く。
内容と閉じフェンスからはフェンス行のインデント幅までが除去される。
CommonMark と同じく、項目の説明に取り込まれるのはインデントされた
フェンスだけで、カラム0(インデントなし)のフェンスは項目の外の
トップレベルのコードブロックとして扱われる。

````markdown
- **`Dir.glob`**:
Expand Down
55 changes: 53 additions & 2 deletions lib/bitclust/markdown_to_rrd.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -419,14 +466,16 @@ 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]+$/
@out << line
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
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 6 additions & 7 deletions lib/bitclust/mdcompiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<dd>'
while /\A(?:\s|\z)/ =~ @f.peek or FENCE_RE =~ @f.peek
while /\A(?:\s|\z)/ =~ @f.peek
case @f.peek
when /\A$/
@f.gets
Expand All @@ -515,8 +518,6 @@ def dd_with_p
line '<p>'
line compile_text(text_node_from_lines(@f.span(DD_TEXT_RE)))
line '</p>'
when FENCE_RE
code_fence
else
raise 'must not happen'
end
Expand All @@ -526,14 +527,12 @@ def dd_with_p

def dd_without_p
line '<dd>'
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 '</dd>'
Expand Down
68 changes: 49 additions & 19 deletions lib/bitclust/rrd_to_markdown.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -375,51 +396,51 @@ 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

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
while @index < @lines.length
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions sig/bitclust/markdown_to_rrd.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions sig/bitclust/rrd_to_markdown.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions test/test_rrd_to_markdown.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading