@@ -45,6 +45,10 @@ def signature_re
4545
4646 MD_ITEM_RE = /\A (\s *)(?:- |\d +\. )/
4747 FENCE_RE = /\A `{3,}/
48+ # インデントされたフェンス(リスト項目・dlist 説明内のコードブロック)
49+ INDENTED_FENCE_RE = /\A ([ \t ]+)(`{3,})/
50+ # dd 段落の継続行(インデント行。ただしインデントフェンスの手前で止まる)
51+ DD_TEXT_RE = /\A [ \t ](?![ \t ]*`{3})/
4852 DLIST_RE = /\A - \* \* (.+?)\* \* :(?:\s |$)/
4953 INFO_RE = /\A - \* \* (?:param|arg|return|raise)\* \* /
5054 SEE_RE = /\A - \* \* SEE\* \* /
@@ -76,6 +80,8 @@ def library_file
7680 raise "@item_stack should be empty. #{ @item_stack . inspect } " unless @item_stack . empty?
7781 when FENCE_RE
7882 code_fence
83+ when INDENTED_FENCE_RE
84+ indented_code_fence
7985 when EMLIST_LEFTOVER_RE
8086 # リスト脈絡などで生のまま残った #@samplecode は前処理で
8187 # //emlist になる。RDCompiler と同じ独立ブロックとして描画する
@@ -135,6 +141,8 @@ def entry_chunk
135141 raise "@item_stack should be empty. #{ @item_stack . inspect } " unless @item_stack . empty?
136142 when FENCE_RE
137143 code_fence
144+ when INDENTED_FENCE_RE
145+ indented_code_fence
138146 when EMLIST_LEFTOVER_RE
139147 emlist
140148 when /\A @todo\b /
@@ -184,16 +192,26 @@ def read_entry_paragraph(f)
184192
185193 def paragraph
186194 line '<p>'
187- line compile_text ( text_node_from_lines ( read_paragraph ( @f ) . map { | l | unescape_hash ( l ) } ) )
195+ line compile_text ( text_node_from_lines ( consume_paragraph_lines { read_paragraph ( @f ) } ) )
188196 line '</p>'
189197 end
190198
191199 def entry_paragraph
192200 line '<p>'
193- line compile_text ( text_node_from_lines ( read_entry_paragraph ( @f ) . map { | l | unescape_hash ( l ) } ) )
201+ line compile_text ( text_node_from_lines ( consume_paragraph_lines { read_entry_paragraph ( @f ) } ) )
194202 line '</p>'
195203 end
196204
205+ # 段落行の読み取り。リストと空行で切り離された残余のインデント行は
206+ # どのブロック処理にも該当しないため、ここで段落として消費する
207+ # (読み取りが空のままだとディスパッチが進まなくなる)
208+ def consume_paragraph_lines
209+ lines = yield . map { |l | unescape_hash ( l ) }
210+ lines = @f . span ( DD_TEXT_RE ) if lines . empty?
211+ lines = [ @f . gets || raise ] if lines . empty? # 最低1行は必ず進める
212+ lines
213+ end
214+
197215 def unescape_hash ( line )
198216 line . sub ( /\A \\ #/ , '#' )
199217 end
@@ -338,25 +356,10 @@ def split_table_row(row)
338356 def code_fence
339357 open_line = @f . gets or raise
340358 fence = open_line [ /\A `+/ ] or raise
341- rest = open_line [ fence . size ..] . to_s . strip
342- lang = nil
343- caption = nil
344- if rest =~ /\A (\w +)?(?:\s +title="((?:[^"\\ ]|\\ .)*)")?\z /
345- lang = $1
346- caption = $2&.gsub ( /\\ (["\\ ])/ , '\1' )
347- end
359+ lang , caption = parse_fence_info ( open_line [ fence . size ..] . to_s . strip )
348360 terminator = /\A `{#{ fence . size } }\s *$/
349361 if lang
350- # caption はタブとして pre の前に置く(rd 側と同期)
351- line "<span class=\" caption\" >#{ escape_html ( caption ) } </span>" if caption
352- line "<pre class=\" highlight #{ lang } \" >"
353- line "<code>"
354- src = +""
355- @f . until_terminator ( terminator ) do |code_line |
356- src << code_line
357- end
358- string highlight_source ( src , lang , caption )
359- line '</code></pre>'
362+ highlighted_fence_body ( lang , caption , terminator )
360363 elsif fence . size == 3
361364 line '<pre>'
362365 @f . until_terminator ( terminator ) do |code_line |
@@ -409,6 +412,51 @@ def collect_fence_lines(terminator)
409412 lines
410413 end
411414
415+ # info string(lang と title="cap")のパース
416+ def parse_fence_info ( rest )
417+ if rest =~ /\A (\w +)?(?:\s +title="((?:[^"\\ ]|\\ .)*)")?\z /
418+ [ $1, $2&.gsub ( /\\ (["\\ ])/ , '\1' ) ]
419+ else
420+ [ nil , nil ]
421+ end
422+ end
423+
424+ # ハイライト付き <pre> の本体。caption はタブとして pre の前に置く
425+ # (rd 側と同期)。strip_re はインデントフェンスのデデント用
426+ def highlighted_fence_body ( lang , caption , terminator , strip_re = nil )
427+ line "<span class=\" caption\" >#{ escape_html ( caption ) } </span>" if caption
428+ line "<pre class=\" highlight #{ lang } \" >"
429+ line "<code>"
430+ src = +""
431+ @f . until_terminator ( terminator ) do |code_line |
432+ src << ( strip_re ? code_line . sub ( strip_re , '' ) : code_line )
433+ end
434+ string highlight_source ( src , lang , caption )
435+ line '</code></pre>'
436+ end
437+
438+ # リスト項目・dlist 説明の中のインデントされたフェンス(GFM のリスト内
439+ # コードブロック)。GFM と同じく、内容と閉じフェンスからフェンス行の
440+ # インデント幅までを除去する
441+ def indented_code_fence
442+ open_line = @f . gets or raise
443+ INDENTED_FENCE_RE =~ open_line or raise
444+ indent = ( $1 || raise )
445+ fence = ( $2 || raise )
446+ lang , caption = parse_fence_info ( open_line [ ( indent . size + fence . size ) ..] . to_s . strip )
447+ terminator = /\A [ \t ]{0,#{ indent . size } }`{#{ fence . size } }\s *$/
448+ strip_re = /\A [ \t ]{1,#{ indent . size } }/
449+ if lang
450+ highlighted_fence_body ( lang , caption , terminator , strip_re )
451+ else
452+ line '<pre>'
453+ @f . until_terminator ( terminator ) do |code_line |
454+ line escape_html ( code_line . sub ( strip_re , '' ) . rstrip )
455+ end
456+ line '</pre>'
457+ end
458+ end
459+
412460 # 箇条書き(- item / N. item、ネスト・継続行あり)
413461 def item_list ( level = 0 )
414462 open_tag = nil
@@ -427,10 +475,16 @@ def item_list(level = 0)
427475 string "<li>"
428476 @item_stack . push ( "</li>" )
429477 string compile_text ( item_line . sub ( /\A \s *(?:-|\d +\. )\s ?/ , '' ) . strip )
430- if /\A (\s +)(?!- |\d +\. )\S / =~ @f . peek
431- @f . while_match ( /\A \s +(?!- |\d +\. )\S / ) do |cont |
478+ # 継続行(折り返しテキスト)とインデントフェンス(項目内コードブロック)
479+ while @f . peek
480+ if INDENTED_FENCE_RE =~ @f . peek
481+ nl
482+ indented_code_fence
483+ elsif /\A \s +(?!- |\d +\. )\S / =~ @f . peek
432484 nl
433- string compile_text ( cont . strip )
485+ string compile_text ( ( @f . gets || raise ) . strip )
486+ else
487+ break
434488 end
435489 end
436490 if ( m = MD_ITEM_RE . match ( @f . peek ) ) && level < ( m [ 1 ] || raise ) . size
@@ -455,9 +509,11 @@ def dd_with_p
455509 case @f . peek
456510 when /\A $/
457511 @f . gets
512+ when INDENTED_FENCE_RE
513+ indented_code_fence
458514 when /\A [ \t ]/
459515 line '<p>'
460- line compile_text ( text_node_from_lines ( @f . span ( / \A [ \t ]/ ) ) )
516+ line compile_text ( text_node_from_lines ( @f . span ( DD_TEXT_RE ) ) )
461517 line '</p>'
462518 when FENCE_RE
463519 code_fence
@@ -472,8 +528,10 @@ def dd_without_p
472528 line '<dd>'
473529 while /\A [ \t ]/ =~ @f . peek or FENCE_RE =~ @f . peek
474530 case @f . peek
531+ when INDENTED_FENCE_RE
532+ indented_code_fence
475533 when /\A [ \t ]/
476- line compile_text ( text_node_from_lines ( @f . span ( / \A [ \t ]/ ) ) )
534+ line compile_text ( text_node_from_lines ( @f . span ( DD_TEXT_RE ) ) )
477535 when FENCE_RE
478536 code_fence
479537 end
@@ -491,21 +549,105 @@ def compile_text(str)
491549
492550 # GFM モードのテキストコンパイル:
493551 # コードスパン `x` → <code>(中身は参照解決しない・HTML エスケープのみ)、
494- # 行頭 **N.** → <strong>。エスケープ済み \` はリテラルのバッククォート
552+ # 行頭 **N.** → <strong>、Markdown リンク(<url> 自動リンク・
553+ # [テキスト](URL)・[テキスト](#アンカー))→ <a>。
554+ # エスケープ済み \` はリテラルのバッククォート
495555 def compile_gfm_text ( str )
496556 hidden = str . gsub ( /\\ `/ , "\x00 " )
497557 hidden . split ( /(`[^`\n ]+`)/ , -1 ) . map { |seg |
498558 if seg =~ /\A `([^`\n ]+)`\z /
499559 "<code>#{ escape_html ( ( $1 || raise ) . gsub ( "\x00 " , '`' ) ) } </code>"
500560 else
561+ links = [ ] #: Array[String]
562+ seg = extract_md_links ( seg , links )
501563 seg = seg . gsub ( /^\* \* (\d +\. )\* \* / , "\x01 \\ 1\x02 " )
502564 rd_compile_text ( MarkdownToRRD . restore_inline ( seg ) )
503565 . gsub ( "\x01 " , '<strong>' ) . gsub ( "\x02 " , '</strong>' )
504566 . gsub ( "\x00 " , '`' )
567+ . gsub ( /\x03 (\d +)\x03 / ) { links [ ( $1 || raise ) . to_i ] || raise }
505568 end
506569 } . join
507570 end
508571
572+ AUTOLINK_RE = %r{<(https?://[^<>\s ]+)>}
573+ # インラインリンクの宛先として受ける形。散文の「[c:String](を参照)」の
574+ # ような括弧書きをリンクと誤認しないため URL と #フラグメントに限る
575+ # (MARKUP_SPEC §7.4/§7.5)
576+ MD_LINK_DEST_RE = %r{\A (?:https?://|\# )}
577+
578+ # Markdown のリンクを描画済み <a> へ退避し \x03idx\x03 プレースホルダに
579+ # 置き換える。後段の rd_compile_text(HTML エスケープ・参照解決)を
580+ # 素通しし、compile_gfm_text の最後で戻す
581+ def extract_md_links ( str , saved )
582+ str = str . gsub ( AUTOLINK_RE ) {
583+ saved << direct_url ( $1 || raise )
584+ "\x03 #{ saved . size - 1 } \x03 "
585+ }
586+ return str unless str . include? ( '[' )
587+ result = +''
588+ i = 0
589+ while i < str . length
590+ if str [ i ] == '\\' && i + 1 < str . length
591+ result << ( str [ i , 2 ] || raise )
592+ i += 2
593+ next
594+ end
595+ if str [ i ] == '['
596+ close = matching_delimiter ( str , i , '[' , ']' )
597+ if close && str [ close + 1 ] == '(' &&
598+ ( dest_end = matching_delimiter ( str , close + 1 , '(' , ')' , space_ends : true ) ) &&
599+ ( dest = str [ ( close + 2 ) ...dest_end ] ) && MD_LINK_DEST_RE =~ dest
600+ saved << md_link ( str [ ( i + 1 ) ...close ] || raise , dest )
601+ result << "\x03 #{ saved . size - 1 } \x03 "
602+ i = dest_end + 1
603+ next
604+ end
605+ end
606+ result << ( str [ i ] || raise )
607+ i += 1
608+ end
609+ result
610+ end
611+
612+ # open 位置の括弧に対応する閉じ括弧の位置(\ エスケープ対応・ネスト可)。
613+ # space_ends: リンク宛先用。空白が現れたらリンクではない(nil)
614+ def matching_delimiter ( str , open , open_char , close_char , space_ends : false )
615+ depth = 0
616+ i = open
617+ while i < str . length
618+ c = str [ i ]
619+ if c == '\\'
620+ i += 1
621+ elsif space_ends && c =~ /\s /
622+ return nil
623+ elsif c == open_char
624+ depth += 1
625+ elsif c == close_char
626+ depth -= 1
627+ return i if depth == 0
628+ end
629+ i += 1
630+ end
631+ nil
632+ end
633+
634+ # 表示テキスト付きリンクの描画。テキストは参照解決しないプレーン表示
635+ # (リンク内リンクは HTML として成立しないため)。外部 URL は rd の
636+ # [[url:]] と同じ external クラス、#アンカーはページ内リンク
637+ def md_link ( text , dest )
638+ label = escape_html ( unescape_md_brackets ( text ) . gsub ( "\x00 " , '`' ) )
639+ href = escape_html ( unescape_md_brackets ( dest ) )
640+ if dest . start_with? ( '#' )
641+ %Q(<a href="#{ href } ">#{ label } </a>)
642+ else
643+ %Q(<a class="external" href="#{ href } ">#{ label } </a>)
644+ end
645+ end
646+
647+ def unescape_md_brackets ( str )
648+ str . gsub ( /\\ ([\[ \] \\ ])/ , '\1' )
649+ end
650+
509651 def restore_rd_text ( str )
510652 # M1 等価モード: md のコードスパンを rd の元表記へ戻して描画する
511653 MarkdownToRRD . restore_text ( str )
0 commit comments