シーケンス図をテキストで出力する

PlantUMLをインストールしてみたのだけど、シーケンス図をテキスト形式で出力時に、日本語が含まれると表示がずれる。
まあ大抵の場合png出力で確認するとはいえ、なんか微妙にはがゆいので、日本語専用でPlantUMLのシーケンス図を出力するスクリプトを書いてみた。
(多分車輪の再発明)

日本 -> ソ連 : 日ソ中立条約
日本 -> アメリカ : 真珠湾攻撃
アメリカ --> 日本 : 原爆投下
ソ連 --> 日本 : 対日参戦
日本 -> 日本 : 無条件降伏

例えば、上記↑のテキストを下記↓の様に出力する。

┏━━┓  ┏━━┓  ┏━━━━┓
┃日本┃  ┃ソ連┃  ┃アメリカ┃
┗┳━┛  ┗┳━┛  ┗━┳━━┛
 ┃日ソ中立条約      ┃
 ┠────→┃      ┃   
 ┃     ┃      ┃   
 ┃真珠湾攻撃┃      ┃
 ┠───────────→┃   
 ┃     ┃      ┃   
 ┃原爆投下 ┃      ┃
 ┃← - - - - - - - - - - -┨   
 ┃     ┃      ┃   
 ┃対日参戦 ┃      ┃
 ┃← - - - -┨      ┃   
 ┃     ┃      ┃   
 ┃無条件降伏┃      ┃
 ┠←─   ┃      ┃   
 ┃     ┃      ┃   
┏┻━┓  ┏┻━┓  ┏━┻━━┓
┃日本┃  ┃ソ連┃  ┃アメリカ┃
┗━━┛  ┗━━┛  ┗━━━━┛
class String
  def jlen # SJISにおけるバイト数を返す #
    len = self.size
    nonAsciiLen = self.gsub(/[\x20-\x7e]/,'').size
    len + nonAsciiLen
  end
end

class Message
  attr_reader :name,:from,:to,:spec
  def initialize(name,from,to,spec=:normal)
    @name,@from,@to,@spec = name,from,to,spec
  end
end

class Participants # メッセージの送り手/受け手のクラスの集合 #
  @@DefOff = 2
  def initialize
    @n = [] # Participantの名前の配列 #
    @p = [] # Participantそのものの配列 #
    @m = [] # Messageの配列 #
  end

  def add(*a)
    i = nil
    a.each do |name|
      i = @n.index(name)
      if !i
        i = @n.size
        loff = 0
        loff = @@DefOff if i>0
        @n << name
        @p << Participant.new(name,loff)
      end
    end
    i
  end

  def add_msg(name,from,to,spec=:normal)
    from = self.add(from)
    to = self.add(to)
    @m << Message.new(name,from,to,spec)
  end

  def draw_top
    @p.each{|p| print ' '*p.loff, p.gen_boxline(:top)			};puts''#┏━┓
    @p.each{|p| print ' '*p.loff, ''+(p.name)+''			};puts''#┃P ┃
    @p.each{|p| print ' '*p.loff, p.gen_boxline(:bottom,'')	};puts''#┗┳┛
  end
  def draw_bottom
    @p.each{|p| print ' '*p.loff, p.gen_boxline(:top,'')		};puts''#┏┻┓
    @p.each{|p| print ' '*p.loff, ''+(p.name)+''			};puts''#┃P ┃
    @p.each{|p| print ' '*p.loff, p.gen_boxline(:bottom)		};puts''#┗━┛
  end

  def draw_msgs
    @m.each do |m|
      start,stop = m.from,m.to
      if m.from > m.to # 送信元が 右側に居る場合 #
        start,stop = m.to,m.from
      end

      # Message#name の描画 #
      (0 ... start).each{|i| print ' '*(@p[i].loff+@p[i].coff), '', ' '*@p[i].roff}
      name,len = m.name,m.name.jlen
      if len%2 == 1
        len += 1
        name += " "
      end
      len /= 2
      print (' '*(@p[start].loff+@p[start].coff)) + '' + name
      (start ... (@p.size-1)).each do |i|
        cnt = @p[i].roff + @p[i+1].loff + @p[i+1].coff
        if len > 0
          cnt,len = cnt-len,len-cnt
        end
        print(' '*cnt) if cnt > 0
        print (len>0) ? ' ' : ''
      end
      puts ''

      # 矢印部の描画 #
      head,body,tail = ' ',' ┃ ',' '
      @p.size.times do |i|
        if i == start
          tail = (m.spec==:normal) ? '' : ' -'
          body = (start==m.from) ? (' ┠'+tail) : ' ┃←'
          body = ' ┠←' if start == stop
        elsif i == stop
          tail = ' '
          body = (stop==m.to) ? '→┃ ' : (head+'┨ ')
        end
        print head*(@p[i].loff+@p[i].coff-1), body, tail*(@p[i].roff-1)
        if i == stop
          head,body,tail = ' ',' ┃ ',' '
        elsif i == start
          head = (m.spec==:normal) ? '' : ' -'
          body = head*3
        end
      end
      puts ''

      draw_lifelines # 行を詰めて表示したい場合はこの行をコメントアウトすべし #
    end
  end
  
  def draw_lifelines
    @p.each{|p| print ' '*(p.loff+p.coff), '', ' '*p.roff	}; puts ''
  end

  def draw
    draw_top
    draw_msgs
    draw_bottom
  end
end

class Participant # メッセージの送り手/受け手のクラス #
  attr_reader :jlen,:name,:loff,:coff,:roff
  def initialize(name, loff=0) # loff : 箱部分の左端の、直前の箱からのオフセット距離 
    @name = name
    @jlen = name.jlen
    if @jlen%2 == 1
      @jlen += 1
      @name += " "
    end
    @jlen /= 2
    @loff = loff
    @coff = (@jlen-1)/2+1
    @roff = (@jlen%2==1) ? @coff : @coff+1
  end

  def gen_boxline(top_or_bottom=:top,tail=nil,len=@jlen) #"┗┳┛" とか "┏━┓"を生成する #
    boxline = ''
    if tail
      boxline = ''*(@coff-1) + tail + ''*(@roff-1)
    else
      boxline = ''*len
    end
    top_or_bottom==:top ? (''+boxline+'') : (''+boxline+'')
  end
end
################################################################
if $0 == __FILE__ and ARGV.size > 0
  p = Participants.new
  ARGF.each do |l|
    next if /^@/ =~ l or /^\s*$/ =~ l
    if /\s+([\-<>]+)\s+/ =~ l
      p1,arrow,p2,mes = $`,$1,$',''
      p2.chomp!;p2.chomp!;p2.sub!(/\s+$/,'')
      if /\s+:\s+/ =~ p2
        p2,mes = $`,$'
      end
      p.add(p1,p2)
      if /^</=~arrow
        p1,p2=p2,p1
      end
      arrow = (/\-\-/=~arrow) ? :dotted : :normal
      p.add_msg(mes,p1,p2,arrow)
    end
  end
  p.draw
end
__END__
#単独で使用する時は以下の様な感じ。
p = Participants.new
#p.add("hogehogeho","foo","日本")
p.add_msg('fooから「日本」へのメッセージ',"foo","日本",:dotted)
p.add_msg('msg2(fooからhogehogeへ)',"foo","hogehogeho",:dotted)
p.add_msg('msg3',"hogehogeho","日本")
p.add_msg('msg4',"part1","part2")
p.add_msg('msg5',"part2","foo",:dotted)
p.draw