Text::VimColor on Ruby and Windows(2)

あれか? 魔改造は俺の病気か何かか?
しかもはてダラといっしょで根っこは人様のコードに頼りっきりっ
てんだからまあ。

vimcolor.rb(魔改)

こんな感じに。
ライセンスは元々と同じでGPLです。


改造のメモとか。

・メソッド分割
・Vimの位置
・run_fileのインタフェースのちょい変更
・gsubの連続による置換をgsub+テーブルに置き換え
・元々Win32用forkを使うつもりだったが、なんかプロセスの無間地獄になってうわあああぁぁぁあ
・なのでWin32用Open3を使うことに
・XMLエスケープの実装をモジュールに
・Tempfileのリソース管理をブロックで
#
# A ported version of Perl's Text::VimColor to Ruby.
#
# modified by arikui(shionist@yahoo.co.jp)
#

require 'strscan'
require 'tempfile'

begin
  require 'win32/open3'
rescue LoadError
  require 'open3'
end



class VimColor
  VIM_COMMAND = File.join(ENV['VIM'], 'vim')
  VIM_OPTIONS = %w[-R -X -Z -i NONE -u NONE -N]
  VIM_PRESET  = ["+set nomodeline", '+set expandtab'] # +set shiftwidth
  VIM_POSTSET = [":let b:is_bash=1", ":filetype on"]
  VIM_MARK_SCRIPT = File.join(File.dirname(__FILE__), 'vimcolor', 'mark.vim')
  
  private
  
  def vim_input(path)
    <<-EOS
#{@vim_postset.join("\n")}
:source #{VIM_MARK_SCRIPT}
:write! #{path}
:qall!
    EOS
  end
  
  def initialize(command = VIM_COMMAND, options = VIM_OPTIONS,
                 preset  = VIM_PRESET , postset = VIM_POSTSET)
    @vim_command = command
    @vim_options = options
    @vim_preset  = preset
    @vim_postset = postset
  end
  
  def vim_execute(path)
    result = nil
    Tempfile.open('ruby-vimcolor') do |t|
      t.puts vim_input(t.path)
      t.flush
      args = [@vim_options, '-s', t.path, path, @vim_preset].flatten
      Open3.popen3("#{@vim_command} #{args.join(' ')}") do |inn, out, err|
        inn.close_write ; out.read ; err.read
      end
      t.rewind
      result = t.read
    end
    result
  end
  
  ARRANGE = { '&l' => '<', '&g' => '>', '&a' => '&' }
  
  def format_arrange(vimout, formatter)
    s = StringScanner.new(vimout)
    tbl = ARRANGE #optimize
    while until_matched = s.scan_until(/>.*?>/)
      matched = s.matched
      text = until_matched[0..(-matched.length - 1)]
      type = matched[1..(matched.length - 2)]
      if s.scan_until(/.*?<#{Regexp.escape(type)}</s)
        text << s.matched[0..-matched.length-1]
        text = text.gsub(Regexp.union(*tbl.keys)){|m| tbl[m] }
        formatter.push(type, text)
      end
    end
    formatter.result
  end
  
  public
  
  #options <- Hash-like one (Hash, Struct, ...)
  def run_file(path, options, formatter_class, *formatter_args)
    formatter_class.respond_to?(:new) or
      formatter_class = self.class.const_get("Format_#{formatter_class}")
    @vim_postset << ":set filetype=#{options[:filetype]}" if options[:filetype]
    @vim_preset  << "+set encoding=#{options[:encoding]}" if options[:encoding]
    vimout = vim_execute(path)
    format_arrange(vimout, formatter_class.new(*formatter_args))
  end
  
  def run(str, file_type, formatter_class, *formatter_args)
    Tempfile.open('ruby-vimcolor-input') do |t|
      t.write(str)
      t.flush
      run_file(t.path, file_type, formatter_class, *formatter_args)
    end
  end
  
  def run_stream(stream, file_type, formatter_class, *formatter_args)
    run(stream.read, file_type, formatter_class, *formatter_args)
  end
  
  
  class Format_array
    def initialize
      @result = []
    end
    
    def push(type, text)
      @result.push [type, text]
    end
    
    attr_reader :result
  end
  
  
  module EscapeXML
    ESC = {
      "&" => "&amp;",
      "<" => "&lt;",
      ">" => "&gt;",
      "'" => "&#39;",
      '"' => "&quot;"
    }
    
    def escape_xml(text)
      tbl = ESC #optimize
      text.gsub(Regexp.union(*tbl.keys)){|m| tbl[m] }
    end
  end
  
  
  class Format_xml
    include EscapeXML
    
    def initialize
      @result = ''
    end
    
    def push(type, text)
      type = 'Normal' if type.empty?
      @result << %[<#{type}>#{escape_xml(text)}</#{type}>]
    end
    
    attr_reader :result
  end
  
  
  class Format_html
    include EscapeXML
    
    def initialize(class_prefix = 'syn')
      @result = ''
      @prefix = class_prefix
    end
    
    def push(type, text)
      text = escape_xml(text)
      @result.concat(
        type.empty? ?
          text : %[<span class="#{@prefix}#{type}">#{text}</span>]
      )
    end
    
    attr_reader :result
  end
  
  
  class Format_ansi
    AnsiCodes = {
      :normal        =>  0,
      :reset         =>  0,
      :bold          =>  1,
      :dark          =>  2,
      :italic        =>  3,
      :underline     =>  4,
      :blink         =>  5,
      :rapid_blink   =>  6,
      :negative      =>  7,
      :concealed     =>  8,
      :strikethrough =>  9,
      :black         => 30,
      :red           => 31,
      :green         => 32,
      :yellow        => 33,
      :blue          => 34,
      :magenta       => 35,
      :cyan          => 36,
      :white         => 37,
      :on_black      => 40,
      :on_red        => 41,
      :on_green      => 42,
      :on_yellow     => 43,
      :on_blue       => 44,
      :on_magenta    => 45,
      :on_cyan       => 46,
      :on_white      => 47,
    }
    
    def initialize(colors = {})
      @result = ''
      @colors = Hash.new([])
      @colors.merge!({
        'Comment'    => [ :blue ],
        'Constant'   => [ :red ],
        'Identifier' => [ :green  ],
        'Statement'  => [ :yellow ],
        'PreProc'    => [ :magenta ],
        'Type'       => [ :green ],
        'Special'    => [ :magenta ],
        'Underlined' => [ :underline ],
        'Error'      => [ :red ],
        'Todo'       => [ :black, :on_yellow ],
      })
      @colors.merge!(colors)
    end
    
    def push(type, text)
      seq = ''
      codes = @colors[type]
      codes.unshift(:reset)
      codes.each {|c|
        num = AnsiCodes[c]
        seq << "\e[#{num}m" if num
      }
      @result << seq << text
    end
    
    def result
      @result << "\e[0m"
      @result
    end
  end
  
end

vc.rb

こっちはvimcolor-embedみたいなコマンド。
本家のはちょっとノリが合わなかったので。

これはイチからスクラッチなのでRuby'sライセンスで。

Usage: vc [options] [FILE]
    -t, --filetype=SYNTAX            assist Vim to guess syntax type
    -f, --format=FORMAT              set output format(ansi|xml|html=default)
    -o, --output=PATH                output to a file which specfied
    -e, --encoding=CODE              set encoding(default=UTF-8)
    -h, --help                       show this help
#!/usr/bin/env ruby

require 'vimcolor'
require 'optparse'


def main
  options = create_option()
  opt = create_optionparser(
    File.basename($0, '.rb'),
    "Usage: %s [options] [FILE]",
    ['-t',
     '--filetype=SYNTAX',
     'assist Vim to guess syntax type',
     lambda{|x| options.filetype = x }],
    ['-f',
     '--format=FORMAT',
     'set output format(ansi|xml|html=default)',
     lambda{|x| options.format = x }],
    ['-o',
     '--output=PATH',
     'output to a file which specfied',
     lambda{|x| options.output = x }],
    ['-e',
     '--encoding=CODE',
     'set encoding(default=UTF-8)',
     lambda{|x| options.encoding = x }],
    ['-h',
     '--help',
     'show this help',
     lambda{ puts opt.help ; exit }]
  )
  begin
    opt.parse!(ARGV)
    file = nil if (file = ARGV.shift) == '-'
    ARGV.empty? or
      raise(ArgumentError, "too many arguments (#{ARGV.size + 1} for 1)")
  rescue OptionParser::ParseError, ArgumentError => ex
    opt.error(ex.message)
  end
  write_file(options.output) do |f|
    f.puts VimColor.new.send("run_#{file ? 'file' : 'stream'}",
      file || $stdin,
      #{ :filetype => options.filetype, :encoding => options.encoding },
      options,
      options.format
    )
  end
end



# # # # # # # # # # # # # # # # #

def write_file(path, &block)
  path == '-' ? yield($stdout) : File.open(path, 'w', &block)
end

def create_option
  default = {
    :filetype   => nil,
    :format     => 'html',#:ansi,
    :output     => '-',
    :encoding   => 'utf-8',
  }
  options = Struct.new(*default.keys).new
  default.each{|k, v| options[k] = v }
  options
end

def create_optionparser(name, banner, *on)
  opt = OptionParser.new
  opt.program_name = name
  opt.banner = sprintf(banner, name)
  def opt.error(msg=nil)
    $stderr.puts "#{program_name}: #{msg}" if msg
    $stderr.puts help
    exit 1
  end
  on.each do |short, long, desc, block|
    opt.on(short, long, desc, &block)
  end
  opt
end



# # #
main