Onigmo の \G アンカーによる字句走査

ruby の組み込み正規表現オブジェクト Regexp には match メソッドが一つあるだけです。 このメソッドの第 1 引数に文字列を、 第 2 引数には検索開始位置を文字単位で指定します。 第 2 引数は省略可能でそのときはゼロを指定したことになります。 match メソッドは、 onig_search 関数を使って、 文字列の検索開始位置以降にある正規表現パターンにマッチする箇所を探します。

    Regexp#match(string, pos = 0): (String, Integer) -> MatchData | nil

python正規表現オブジェクトには search メソッドと match メソッドの 2 つがあります。 search メソッドは ruby の match メソッドと同じで、 検索開始位置以降からマッチする箇所を探します。 一方、 python の match メソッドは検索開始位置でマッチするかどうかを調べます。 Javascriptpython と同じで、 2 種類のメソッドをもっています。

python の match メソッドは、 字句解析に使うと便利です。 ruby では、 strscan ライブラリで、 python の match メソッドの利用パターンをカプセル化していました。 strscan の scan メソッドも内部では python の match メソッドに相当する onig_match 関数を使って実装してあります。 さらに、 ruby 1.9 以降では、 \G アンカーの有無で、 python の search と match のように、 Regexp オブジェクトの match メソッドを使い分けられるようになっています。

  python:
  import re
  re.compile(r'\d+').search('   1234   ').group(0)    #=> '1234'
  re.compile(r'\d+').match('   1234   ')              #=> None
  re.compile(r'\d+').match('   1234   ', 3).group(0)  #=> '1234'

  ruby1.9 以降:
  %r/\d+/.match('   1234   ').to_a[0]                 #=> '1234'
  %r/\G\d+/.match('   1234   ').to_a[0]               #=> nil
  %r/\G\d+/.match('   1234   ', 3).to_a[0]            #=> '1234'

StringScanner にカプセル化してあるとはいっても、 バイト単位で文字列を扱うため、 UTF-8 エンコーディングでの使い勝手が悪いのが欠点です。 さらに、 戻り読みも利用できません。 リファレンス・マニュアルに \G アンカーが記載されないので、 これまでは StringScanner を我慢して使ってきましたが、 ruby 1.9 で組み込まれてから ruby 2.6 を迎えようとしています。 その間、 削除されることなく利用可能なまま過ぎています。 そろそろ使っても平気だろうと判断しました。 ただし、 そのまま match メソッドを使うのではなく、 StringScanner のように、 文字列と検索開始位置を組にして、 マッチしたら検索開始位置を進めていく小さなクラスを使うことにします。

class MiniStrScanner
  attr_reader :string, :pos, :last_match

  def initialize(str)
    @string = str
    @pos = 0
    @last_match = nil
  end

  def [](i) @last_match && @last_match[i] end
  def begin(i) @last_match && @last_match.begin(i) end
  def end(i) @last_match && @last_match.end(i) end

  def eos?() @pos >= @string.size end

  def match?(regexp)
    @last_match = regexp.match(@string, @pos)
  end

  def scan(regexp)
    @last_match = regexp.match(@string, @pos) or return nil
    @pos = @last_match.end(0)
    self
  end
end

StringScanner で戻り読みパターンが利用できない理由

添付ライブラリ strscan 0.7.0 ではパターンに戻り読みが使えません。 理由は簡単で、 次の引用箇所のように、 検索対象文字列に指定するアドレスに、 文字列先頭アドレス (S_PBEG マクロ) ではなくスキャン・ポインタに対応するアドレス (CURPTR マクロ) を渡しているためです。 検索器がそこから前に文字列が存在しないものとみなすため、 戻り読みは失敗しますし、 失敗するべきです。

strscan.c の strscan_do_scan () 関数

#define S_PBEG(s) (RSTRING_PTR((s)->str))
#define CURPTR(s) (S_PBEG(s) + (s)->curr)
#define S_RESTLEN(s) (S_LEN(s) - (s)->curr)

    ret = onig_match(re,                                    /* 正規表現オブジェクト */
                     (UChar* )CURPTR(p),                    /* 検索対象文字列 */
                     (UChar* )(CURPTR(p) + S_RESTLEN(p)),   /* 同上の終端アドレス */
                     (UChar* )CURPTR(p),                    /* 同上の検索アドレス */
                     &(p->regs),                            /* マッチ領域情報 */
                     ONIG_OPTION_NONE);                     /* 検索時オプション */

そこで、 戻り読みが必要な場合は、 スキャン・ポインタを必要文字数分戻して match? メソッドを使います。 例えば、 CommonMark の強調文字マーク (星印限定) を判別するときは 1 文字戻します。

# -*- coding: utf-8 -*-
require 'strscan'

def commonmark_flanking(strscan)
  r = 0
  if src.bol?
    if src.match?(%r/[*]++\P{Space}/)
      r = 1
    end
  else
    p0 = src.pos
    begin
      src.pos -= 1
    end while 0x80 == (src.string.getbyte(src.pos) & 0xe0)
    if src.match?(%r/.[*]++[^\p{Space}\p{Punct}]/)
      r = 1
    elsif src.match?(%r/[\p{Space}\p{Punct}][*]++\p{Punct}/)
      r = 1
    end
    if src.match?(%r/[^\p{Space}\p{Punct}][*]++/)
      r += 2
    elsif src.match?(%r/\p{Punct}[*]++[\p{Space}\p{Punct}]/)
      r += 2
    end
    src.pos = p0
  end
  0 == r ? nil : [0 != (r & 1) ? :left : nil, src.scan(%r/[*]+/), 0 != (r & 2) ? :right : nil]
end

strscan を戻り読み対応にするには、 Onigmo API では検索対象文字列アドレスと検索開始アドレスを別に指定できるので、 次のように修正するだけで良いだろうと思われます。 ただし、 この修正で互換性が損なわれないかどうかを調べる必要があるでしょう。

     if (headonly) {
-        ret = onig_match(re, (UChar* )CURPTR(p),
+        ret = onig_match(re, (UChar* )S_PBEG(p),
                          (UChar* )(CURPTR(p) + S_RESTLEN(p)),
                          (UChar* )CURPTR(p), &(p->regs), ONIG_OPTION_NONE);
     }
     else {
         ret = onig_search(re,
-                          (UChar* )CURPTR(p), (UChar* )(CURPTR(p) + S_RESTLEN(p)),
+                          (UChar* )S_PBEG(p), (UChar* )(S_PBEG(p) + S_LEN(p)),
                           (UChar* )CURPTR(p), (UChar* )(CURPTR(p) + S_RESTLEN(p)),
                           &(p->regs), ONIG_OPTION_NONE);
     }

Markdown ブロックの WikiWikiWeb 方式での書式変換

これまで書いた Markdown 書式変換器は、 John Gruber によるPerl によるリファレンス実装入れ子認識法を参考にしていました。 そこでは、 ブロックを構成する一連の行の行頭マークを一つずつ削りながら下の行へと降りていき、 削り終えたら、 削り初めた行に戻って、 さらに一連の行の行頭マークを削っていくという、 行をいったりきたりする動きをしていました。 カラム主導で、 ブロックの入れ子構造を扱う方式になっていると言えます。 この方式を PEG-MarkdownSundownDiscount も採用しています。

一方、 インデントの深さで入れ子構造を表すのは元祖 WikiWikiWeb の書式も一緒ですけど、 こちらでは行を上から下へと一度降りていくだけで入れ子になった HTML ブロック要素に変換します。 元祖 WikiWikiWeb の最初の処理系 WikiBase では行頭のタブ文字の個数の変化と、 入れ子になっている HTML タグ名のスタックの両方を使ってブロックの入れ子構造を判別するようになっていました。 入力テキストの行の並びに沿って処理が進んでいく点では、 WikiWikiWeb 方式の方が素直です。

なお、 WikiBase は Ward CunninghamFederated Wiki の開発に転じてから公開されていたソースコードを読むことができなくなってます。 ですが、 かって Perl で記述してあった書式変換部は元祖 WikiWikiWeb c2 wiki の閲覧 HTML ページの Javascript に移植されていて、 Perl の元のコードの入れ子を扱う部分はコメントになって読むことができます。

http://wiki.c2.com/ ページ・ソースの javascriptenter()bullets()

そこで、 Markdown でも行ごとにマーク・スタックを作って、 それの変化でブロックの入れ子構造を判別することを試みます。 今回は、 ブロック要素の変換だけとします。 インライン要素は変換せず、 HTML エスケープするだけにします。 CommonMark 規格への対応は考えないことにします。

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

src = <<'EOS'
Mark Stack Markdown
===================

plain
      paragraph
                  here.

* * *

  * list item 0
  * list item 1
lazy lines of item1.

      * list item 1-0
      * list item 1-1
   
    > heading
    > -------
    >
    > nested blockquote

> top level blockquote
lazy lines of blockquote
>
>   * nested list item 2-0
>   * nested list item 2-1
>
>     > blockquote list item blockquote
>     > ok?
>
> > block quote block quote.
> and its lazy lines
>
> not a lazy lines.
EOS
Block.new(src).convert

Markdown は行を折りたたんで、 本来は一つの長い行を複数の行に折り返す (lazy lines) ことができるようになっているので、 マーク・スタックをブロック単位と行単位の両方に設けておきます。 そして、 状態変数を使って折り返してある複数の行を追跡していきます。

# -*- coding: utf-8 -*-

#@<MiniStrScanner@>

class Block
  def initialize(string)
    string.chomp!
    string << "\n\n"    # HTML ブロック認識のためのダミーの空行
    @src = MiniStrScanner.new(string)

    @last = []          # 最後に HTML 要素に変換したブロックのマーク・スタック
    @nest = []          # 現在のブロックのマーク・スタック
    @mark = []          # 現在の行のマーク・スタック
    @inline = []        # 現在のブロックのインライン行のピース・テーブル
    @blanks = []        # インデント・コード・ブロックの空行を溜めるピース・テーブル
    @state_lazy = :S    # 折り返し行のための状態 :S は最初の行
                        #                        :P は P 要素の2行め以降、
                        #                        :L は LI 要素の2行め以降
    @bol_out = true     # HTML の出力で、 行頭の出力になっているとき真
  end

#@<convert@>
#@<indentation@>
#@<block_mark@>
#@<mark_fold@>
#@<setext_heading_fix@>
#@<blank_or_lazy_lines@>
#@<indented_code@>
#@<block_backtick@>
#@<html_block_element@>
#@<reference_link_definition@>
#@<markup_block@>
#@<markup_inline@>
#@<open_block@>
#@<close_block@>
#@<close_nest@>
#@<output utilities@>
end

MiniStrScanner は \G アンカ付き正規表現で字句を切り出すために、 文字列とそれの検索開始位置を組にしたオブジェクトです。 ruby 添付ライブラリ strscan を参考にしています。

class MiniStrScanner
  attr_reader :string, :pos, :last_match

  def initialize(str) @string, @pos, @last_match = str, 0, nil end
  def [](i) @last_match && @last_match[i] end
  def begin(i) @last_match && @last_match.begin(i) end
  def end(i) @last_match && @last_match.end(i) end
  def eos?() @pos >= @string.size end
  def bol?() 0 == @pos || "\n" == @string[@pos - 1] end

  def scan(regexp)
    @last_match = regexp.match(@string, @pos) or return false
    @pos = @last_match.end(0)
    self
  end
end

Markdown でのマーク・スタックの作り方から考えます。 入れ子の器になるのは順序なしリストと順序付きリスト、 さらにブロック・クォートの 3 種類です。 他の要素は器にならず、 入れ子で包まれた餡になります。 ブロック・クォートは > 記号一つで一段階の入れ子を表します。 行ごとに同じ入れ子記号が現れるのは WikiWikiWeb と同じです。 一方、 リストは空白によるインデントで入れ子を表します。 こちらは、 同じカラムに必ずリスト・マークを受け継いでいくことにして、 現在のブロックのマーク・スタックから現在の行のそれへ転記していけば良いでしょう。 next_lazy は現在の行が平文 (TX) のときに state_lazy へするべき状態です。 インデントの最後がリストなら L、 それ以外は P とします。

#@<indentation@>=
  def indentation()
    same, next_lazy = true, :P
    i = 0
    while true
      if same && (:UL == @nest[i] || :ul == @nest[i]) && @src.scan(%r/\G[ ]{4}/)
        @mark << :ul
        next_lazy = :L
      elsif same && (:OL == @nest[i] || :ol == @nest[i]) && @src.scan(%r/\G[ ]{4}/)
        @mark << :ol
        next_lazy = :L
      elsif @src.scan(%r/\G[ ]{0,3}>[ ]?/)
        @mark << '>'
        if '>' != @nest[i] then same = false; @lazyness = :S end
        next_lazy = :P
      else
        break
      end
      i += 1
    end
    [same, next_lazy]
  end

リストの場合、 リスト開始記号で始まる行のマークは大文字 (UL、OL)、 転記した場合は小文字 (ul、ol) を使って区別します。 逆に UL と ul を同じ扱いにしたいときに mark_fold メソッドを使います。

#@<mark_fold@>=
  def mark_fold(a) :ul == a ? :UL : :ol == a ? :OL : a end

入れ子の餡になる要素は、 行頭マークで判別できるものと、 文脈によって決まるものとに分かれます。 折り返し行の中では行頭マークの扱いにならない記号もあるので、 状態変数でガードします。 どれとも一致しないものは平文 (TX) です。

#@<block_mark@>=
  def block_mark()
    if @src.scan(%r/\G[ ]*$/)
      @mark << :BL
    elsif :S == @lazyness && @src.scan(%r/\G[ ]{4}/)
      @mark << ' '
    elsif :P != @lazyness && @src.scan(%r/\G[ ]{0,3}(?:(?:[*][ ]*){3,}|(?:-[ ]*){3,}|(?:_[ ]*){3,})$/)
      @mark << :hr
    elsif :P != @lazyness && @src.scan(%r/\G[ ]{0,3}[-+*][ ]+(?=\S)/)
      @mark << :UL
    elsif :P != @lazyness && @src.scan(%r/\G[ ]{0,3}[0-9]+[.][ ]+(?=\S)/)
      @mark << :OL
    elsif :S == @lazyness && @src.scan(%r/\G[ ]{0,3}(\#{1,6})[ ]+(?=\S)/)
      @mark << "h#{@src[1].size}".intern
    else
      @src.scan(%r/\G[ ]*/)
      @mark << :TX
    end
    @mark[-1]
  end

この 2 つで現在の行から直訳したマーク・スタックができます。 平文の扱いは状態変数で決まります。 空行と折り返された行ではマーク・スタックが不完全なので、 現在のブロックのマーク・スタックを丸ごと受け継ぐことにします。

#@<convert@>=
  def convert()
    while ! @src.eos?
      @src.bol? or raise 'beginning of line fail.'
      @nest, @mark = @mark, []
      if :S == @lazyness
        next if block_backtick()
        next if html_block_element()
        next if reference_link_definition()
      end
      same, next_lazy = indentation()
      sym = block_mark()
      fst = @src.pos
      snd = @src.scan(%r/\n/).end(0)
      if :S != @lazyness && :TX == sym
        @mark = @nest
        @inline << [fst, snd]       # 折り返し行 (lazy lines)
      else
        setext_heading_fix()
        blank_or_lazy_lines(same, next_lazy)
        if ' ' == @mark[-1] && @nest == @mark
          indented_code(sym, fst, snd)
        else
          markup_block(@nest)
          @inline.clear             # 新しいブロック開始
          @inline << [fst, snd] if :BL != sym
          @blanks.clear if ' ' == @mark[-1]
        end
      end
    end
    markup_block(@mark)
    close_nest(@last, 0)
  end

Setext のヘディングは 2 行目に下線を引いてあります。 そこで、 折り返し行としてとりあえずインラインへ読み進めておいて、 インラインの行数が 2 行で、 2 行目が下線になっているものをヘディングへ変更することにします。 ここまでの書き方では、 段落は Setext ヘディングに常に変わります。 リスト項目では 2 重線しかヘディングにならず、 1 重線は水平線区切りになります。

#@<setext_heading_fix@>=
  def setext_heading_fix()
    if :S != @lazyness && 2 == @inline.size
      fst, snd = @inline[1]
      if %r/\A[ ]*(?:(=)=*|(-)-*)[ ]*\n/ =~ @src.string[fst ... snd]
        @nest.pop if :p == @nest[-1]
        @nest << ($1 ? :h1 : :h2)
        @inline.pop
        @mark = @nest
      end
    end
  end

マーク・スタックの調整をさらにおこないます。 空行のマーク・スタックは不完全なので、 折り返し行と同じく、 現在のブロックのマーク・スタックを受け継ぐことにします。 既に折り返し行の判別が済んでいるので、 この時点に残った平文は段落 (P 要素) の最初の 行になっていることが確定します。 リスト要素は、 リスト要素であることは確定しているのですが、 Markdown では最初の要素によってリストの種類を決定し、 その後のマークの違いを無視する流儀です。 そこで、 現在のブロックの同じレベルがリスト要素ならば、 それで上書きします。 ちなみに、 mark_fold が小文字を大文字にするのは、 この上書きに対応するためなのでした。

#@<blank_or_lazy_lines@>=
  def blank_or_lazy_lines(same, next_lazy)
    @lazyness = :S
    case @mark[-1]
    when :BL
      @mark = @nest
    when :TX
      @mark[-1] = :p
      @lazyness = next_lazy
    when :UL, :OL
      i = @mark.size - 1
      if same && i < @nest.size
        block_mark = mark_fold(@nest[i])
        if :UL == block_mark || :OL == block_mark
          @mark[-1] = block_mark
        end
      end
      @lazyness = :L
    end
  end

上で、 空行のマーク・スタックはブロックのものを受け継いだため、 インデントによるコード・ブロック内の行はすべてスペースでマークしてあることになります。 順次、 コード・ブロックのインライン配列に追加していきます。 ただし、 ブロックの末尾の空行をインライン配列に追加するのを避ける目的で、 空行溜に空行を一時的に入れておきます。 空行ではない行を追加する前に空行溜から移すことにします。

#@<indented_code@>=
  def indented_code(sym, fst, snd)
    if :BL == sym
      @blanks << [fst, snd]
    else
      @inline.concat(@blanks); @blanks.clear
      @inline << [fst, snd]
    end
  end

ここまで、 マーク・スタックを作る行の扱いは終わりです。 他に、 Markdown にはマーク・スタックを作らないものがあります。 バック・チック 3 連で挟むコード・ブロックがその一つです。 正規表現で必要なものをすくい上げてから、 pre 要素を出力します。

#@<block_backtick@>=
  BLOCK_BACKTICK = %r/\G```[ ]*(\S+)?[ ]*\n(.*?)\n```\n/m

  def block_backtick()
    @src.scan(BLOCK_BACKTICK) or return false
    lang = (@src[1].nil?) ? '' : %Q{ class="language-#{escape_html(@src[1])}"}
    markup_block(@nest)
    print_nl_block "<pre#{lang}><code>"
    print escape_html(@src[2])
    print_block "</code></pre>\n"
    true
  end

HTML のブロック要素も同じやりかたにします。 正規表現で切り出して、 直接出力します。

#@<html_block_element@>=
  HTML_ATTRIBUTE = %r{
    [a-zA-Z0-9:.-]+[ \n]*(?:=[ \n]*(?:"[^<>"]*"|'[^<>']*'|`[^<>`]`|[a-zA-Z0-9:.-]+))?
  }mx
  HTML_BLOCK = %r{\G
  (?:<!--.*?-->
  |  <(blockquote|del|div|dl|figure|h[1-6]|ins|ol|p|pre|table|ul)
     (?:[ \n]+#{HTML_ATTRIBUTE})*[ \n]*>.*?<\/\1[ \n]*>
  |  <hr(?:[ \n]+#{HTML_ATTRIBUTE})*[ \n]*\/?>
  )[ ]*\n\n
  }imxo

  def html_block_element()
    @src.scan(HTML_BLOCK) or return false
    markup_block(@nest)
    print_block @src.string[@src.begin(0) ... @src.end(0) - 1]
    true
  end

インライン要素用のリファレンス・リンク定義は、 今回は読み飛ばします。 これの場合は、 空行と同じ扱いとして、 行のマーク・スタックにブロックのマーク・スタックを継承しておきます。

#@<reference_link_definition@>=
  REFLINK_DEF = %r{\G
    [ ]{0,3}\[([^\s\[\]]+(?:[ ]+[^\s\[\]]+)*)\]:[ ]+(?:(\S+)|<([^\n<>]+)>)
    (?:(?:[ ]+(?:\n[ ]*)?|\n[ ]*)
       (?:"([^\n]*)"|'([^\n]*)'|\(([^\n]*)\)))?[ ]*\n
  }mx

  def reference_link_definition()
    @src.scan(REFLINK_DEF) or return false
    reftitle = @src[4] || @src[5] || @src[6]
    refuri = @src[2] || @src[3]
    refsign = @src[1].gsub(%r/[ ]+/, ' ').downcase
    @mark = @nest
    true
  end

これまで何回か既に現れてきた markup_block メソッドで、 マーク・スタックとインラインから HTML ブロック要素を作ります。 引数の nest が現在のブロックのマーク・スタックです。 まず、 これの入れ子の器になれる要素の個数を n1 に求めます。 さらに、 直前に出力したブロックのマーク・スタック (last と比較し、 最初に見つかった異なるマークのある添字を i0 に求めます。 直前のブロックのマーク・スタックから i0 より深いレベルのマークを使って HTML の終了タグを出力します。 今度は現在のブロックのマーク・スタックから i0 より深いレベルのマークを使って HTML 開始タグを出力します。 インラインを出力し、 その後、 入れ子の器になれないブロックに限って HTML の終了タグを出力して、 HTML ブロック要素の出力を終わります。

#@<markup_block@>=
  def markup_block(nest)
    not @inline.empty? or return
    n1 = nest.size
    if n1 > 0 && '>' != nest[-1]  && :UL != mark_fold(nest[-1]) && :OL != mark_fold(nest[-1])
      n1 -= 1
    end
    i0 = 0
    while i0 < @last.size && i0 < n1 && mark_fold(@last[i0]) == mark_fold(nest[i0])
      i0 += 1
    end
    close_nest(@last, i0)
    if ! nest.empty?
      open_block(nest, i0)
      if :hr != nest[-1]
        markup_inline(nest)
        close_block(nest) if n1 < nest.size
      end
      nest.pop if n1 < nest.size
    end
    @last.replace(nest)
  end

インライン要素の変換は、 今回はパスしています。 代用として HTML エスケープして出力していきます。 なお、 最後の行だけは改行を出力しないようにしています。

#@<markup_inline@>=
  def markup_inline(nest)
    @inline.each_index {|i|
      fst, snd = @inline[i]
      snd -= 1 if i + 1 == @inline.size && "\n" == @src.string[snd - 1]
      print_block escape_html(@src.string[fst ... snd])
    }  
  end

マーク・スタックの共通部分の末尾が UL か OL のときは、 リスト開始マークのついたブロックを表しており、 前のリスト項目を閉じて、 新しくリスト項目を始めなければなりません。 他は、 新しく加わったマークなので、 それに対応する開始タグを出力します。

#@<open_block@>=
  def open_block(nest, i0)
    if i0 > 0 && (:UL == nest[i0-1] || :OL == nest[i0-1])
      print_block "</li>\n<li>"
    end
    while i0 < nest.size
      case nest[i0]
      when :UL then print_nl_block "<ul>\n<li>"
      when :OL then print_nl_block "<ol>\n<li>"
      when :ul, :ol then nil
      when '>' then print_nl_block "<blockquote>\n"
      when ' ' then print_nl_block "<pre><code>"
      when :hr then print_nl_block "<hr />\n"
      else print_nl_block "<#{nest[i0]}>"
      end
      i0 += 1
    end
  end

入れ子の器にならないブロック要素はインラインの出力後にタグを閉じます。

#@<close_block@>=
  def close_block(nest)
    case nest[-1]
    when ' ' then print_block "</code></pre>\n"
    else print_block "</#{nest[-1]}>\n"
    end
  end

入れ子の器になるブロック要素のタグは後で閉じることにします。

#@<close_nest@>=
  def close_nest(last, i0)
    i = last.size
    while i0 < i
      i -= 1
      case last[i]
      when :UL, :ul then print_block "</li>\n</ul>\n"
      when :OL, :ol then print_block "</li>\n</ol>\n"
      when '>' then print_nl_block "</blockquote>\n"
      end
    end
  end

残りは出力のための補助メソッドです。 エスケープ処理と、 改行出力調整付きの print があります。

#@<output utilities@>=
  def escape_html(s)
    s.gsub(%r/[&<>"]/) {|x|
      '&' == x ? '&amp;' : '<' == x ? '&lt;' : '>' == x ? '&gt;' : '&quot;'
    }
  end

  def print_block(str)
    print str
    @bol_out = ("\n" == str[-1]) if ! str.empty?
  end

  def print_nl_block(str)
    print "\n" if ! @bol_out
    print_block(str)
  end