Markdown ブロックの WikiWikiWeb 方式での書式変換 (その3)

先日の続きです。 葉ブロックの 2 行目以降の行では、 マーク・スタックを作らないため、 ブロック開始行用とは別に、 行頭の字下げを調べるメソッドを用意しています。 先日の setext ヘディングが開始行と同じレイアウトで下線行が字下げされているかどうかを調べるのに、 indent_level メソッドを使っていました。 レイアウトが開始行と同じかどうかを調べるのが目的なので、 このメソッドは、 先頭行とまったく同じ規則でリストによる字下げとブロック引用マークを読んでいきます。 そうして求めた字下げレベルを返します。

  def indent_level(src, mark)
    i = 0
    while ([:UL, :OL, :ul, :ol].member?(mark[i]) && src.scan(%r/\G[ ]{4}/)) \
          || src.scan(%r/\G[ ]{0,3}>[ ]?/)
      i += 1
    end
    i
  end

このメソッドで求めた字下げレベルが先頭行のコンテナ・ブロック部の長さに一致しているときは、 レイアウトが同じになっていることを意味しています。 setext ヘディングかどうかを調べようとしているときのマーク・スタックの末尾に葉ブロック・マークとして平文マークが入っているので、 マーク・スタックのサイズから 1 を引いたものが、 このメソッドで求めたレベルと同じならレイアウトが一致していることになります。

先頭行と同じレイアウトの空行かどうかの判定では、 ブロック引用マークのレイアウトさえ一致していることを調べれば良いので、 字下げレベルが小さくても同じレイアウトになっている可能性があります。 ただし、 字下げレベルよりも深いブロック引用マークが開始行に存在するときは異なるレイアウトになっていると判断します。 setext ヘディングの下線行の次に空行があるかどうかを判定するのに、 このやりかたに従っていました。

同様の判定方法を使って字下げコード・ブロックの内容行を読み込んでいくことができます。 同じレイアウトの空行であることと、 同じコンテナ・ブロックのレイアウトの後に字下げが続いていることで同一のレイアウトかどうかがわかります。 同じレイアウトの空行と字下げ行を、 そのようにして判定して、 それが続く限りインライン配列に取り込んでいきます。 ただし、 この判定方法だと、 字下げブロックの終わりのブロック間の区切りの空行も取り込んでしまうので、 余計に取り込んだ空行を捨てるようにしています。 このレイアウト判定法は、 将来的に、 入れ子になっている HTML ブロックやフェンス挟みコード・ブロックにも利用できることでしょう。

  def indented_code_block()
    trim = 0
    adv = @src.branch
    while not adv.eos?
      i = indent_level(adv, @mark)
      if (i + 1 ... @mark.size).all?{|j| '>' != @mark[j] } && adv.scan(%r/\G[ ]*$/)
        #
      elsif i == @mark.size - 1 && adv.scan(%r/\G[ ]{4}/)
        trim = @inline.size
      else
        break
      end
      inline_push(adv)
      @src.merge(adv)
    end
    @inline[trim + 1 ... @inline.size] = []
  end

一方、 字下げをさぼっても良いことになっているのが不精行なので、 indent_level によるレイアウト判定をおこないません。 例外は引用ブロックのときで、 このときに限って、 indent_level で先読みして、 深いレベルに引用ブロック・マークがあるときは不精行の連なりを切り上げます。

  def lazylines(lazyness)
    blockquote = (@mark.size > 1 && '>' == @mark[-2])
    adv = @src.branch
    while ! adv.eos?
      if blockquote
        break if indent_level(adv.branch, @mark) > @mark.size - 1
      end
      lazylines_indent(adv)
      break if adv.scan(%r/\G[ ]*$/)
      if :LIST == lazyness
        break if adv.scan(%r/\G[ ]{0,3}(?:(?:[*][ ]*){3,}|(?:[-][ ]*){3,}|(?:_[ ]*){3,})$/)
        break if adv.scan(%r/\G[ ]{0,3}[*+-][ ]+(?=\S)/)
        break if adv.scan(%r/\G[ ]{0,3}\d+[.][ ]+(?=\S)/)
      end
      adv.scan(%r/\G[ ]*/)
      inline_push(adv)
      @src.merge(adv)
    end
  end

不精行でも行頭の引用ブロックのマークは読み飛ばすことになっているので、 indent_level に似たメソッドを使います。 開始行のレベルまでの引用ブロック・マークだけを読み飛ばすのが、 異なります。

  def lazylines_indent(src)
    i = 0
    while ([:UL, :OL, :ul, :ol].member?(@mark[i]) && src.scan(%r/\G[ ]{4}/)) \
          || ('>' == @mark[i] && src.scan(%r/\G[ ]{0,3}>[ ]?/))
      i += 1
    end
  end

以上のやりかたで求めたインラインへ取り込む行を、 inline_push で部分文字列の位置を記録します。

  def inline_push(src)
    first = src.pos
    second = src.scan(%r/\n/).end(0)
    @inline << [first, second]
  end

インラインへの取り込みが終わると、 今度は葉ブロックに続くブロック間の区切りの空行を読み飛ばします。 ここでも、 lazylines_indent を使ってコンテナ・ブロックによるインデントを読み飛ばします。 なお、 ここを indent_level に変えると、 一段深い空の引用ブロックを無視するようになります。

  def skip_blank_lines()
    adv = @src.branch
    while not adv.eos?
      lazylines_indent(adv)
      adv.scan(%r/\G[ ]*\n/) or break
      @src.pos = adv.pos
    end
  end

ここまでで求まる、 mark と inline の配列と、 一つ前に出力済みの mark をコピーしておいた nest を使って、 HTML タグを生成します。 前の版の markup_block と内容は同じです。

  def markup_block(mark)
    n1 = mark.size
    if n1 > 0 && '>' != mark[-1] && :UL != up(mark[-1]) && :OL != up(mark[-1])
      n1 -= 1
    end
    i0 = 0
    while i0 < @nest.size && i0 < n1 && up(@nest[i0]) == up(mark[i0])
      i0 += 1
    end
    close_container(@nest, i0)
    if ! mark.empty?
      open_block(mark, i0)
      if :hr != mark[-1]
        markup_inline(mark)
        close_block(mark) if n1 < mark.size
      end
      mark.pop if n1 < mark.size
    end
    @nest.replace(mark)
  end

close_container は前の版の close_nest メソッドを改名しただけで内容は同じです。 それ以外のメソッドはすべて前の版と同じなので省略します。