色付きコードでブログを投稿しよう!

TextMateには言語のシンタックスに従って
ソースコードを自動で色付けするSyntax Highlighting機能がある
複数のThemeが用意されており
そこから自分の好みのカラーセットを選べる
現在の僕のお気に入りはCobalt


Syntax Highlightingはほんとうにソースコードを綺麗にみせる
自分の汚いコードがそれなりの作品に化けたように錯覚する
誰か日本語用Syntax Highlightingを作ってくれないかな
そうしたら自分の日本語ももう少しマシに見えるのに


TextMateを使っていると
このカラーを使った色付きコードでブログの投稿をしたい
という欲求が当然に生じる


幸いなことに
UltravioletというRubyの拡張ライブラリがそれを可能にしてくれる
UltravioletはTextPowというTextMateのBundleを解析するライブラリを使い
その解析結果に従ってソースコードをカラーリングする


早々Ultravioletを使って
HaskellJavascriptLispPerlPythonRubyで書いたコードを投稿してみる
(Ruby以外のコードは他のサイトから拝借しました)*1


fact.hs with espresso_libre

   1  factorial n
2 | n == 0 = 1
3 | otherwise = n * factorial (n-1)

fact.js with spacecadet

   1  var	fact = function(n) {
2 if(n==0) {
3 return 1;
4 } else {
5 return n * arguments.callee(n-1);
6 }
7 }

fact.lisp with brilliance_black

   1  (defun factorial (n)
2 (if (<= n 1)
3 1
4 (* n (factorial (- n 1)))))

fact.pl with amy

   1  sub fact {
2 my($n) = @_;
3
4 if ($n == 0) {
5 return 1;
6 } else {
7 return $n * fact($n - 1);
8 }
9 }
10

fact.py with all_hallows_eve

   1  def fact(n):
2 if n == 0:
3 return 1L
4 else:
5 return n * fact(n - 1)

fact.rb with slush_poppies

   1  def fact(n)
2 (1..n).inject { |mem, var| mem * var }
3 end


Ultravioletのインストールは以下のようにできるけど*2

  $ gem install -r ultraviolet --include-dependencies

TextPowで鬼車(oniguruma)正規表現ライブラリを使っているので*3
予めそのインストールが必要になる*4


Ultravioletでは外部cssファイルを読み出しているので
その出力結果を直接ブログに張り付けるとうまくいかない
だから以下のサンプルコード(hilite.rb)を書いた


hilite.rb with cobalt

   1  #!/usr/bin/env ruby
2 require "rubygems"
3 require "uv"
4
5 class Hilite
6 BGCOLORS = {"all_hallows_eve" => {:bg => '#000000', :fg => '#ffffff'},
7 "amy" => {:bg => '#200420', :fg => '#d1d0ff'},
8 "blackboard" => {:bg => '#0d1021', :fg => '#f8f8f8'},
9 "brilliance_black" => {:bg => '#0d0d0d', :fg => '#cccccc'},
10 "brilliance_dull" => {:bg => '#0a0a0a', :fg => '#cdcdcd'},
11 "cobalt" => {:bg => '#002444', :fg => '#e6e1dc'},
12 "dawn" => {:bg => '#f9f9f9', :fg => '#080808'},
13 "espresso_libre" => {:bg => '#2a211c', :fg => '#bcae9d'},
14 "idle" => {:bg => '#ffffff', :fg => '#000000'},
15 "iplastic" => {:bg => '#efefef', :fg => '#000000'},
16 "lazy" => {:bg => '#ffffff', :fg => '#000000'},
17 "mac_classic" => {:bg => '#ffffff', :fg => '#000000'},
18 "magicwb_amiga" => {:bg => '#969696', :fg => '#000000'},
19 "pastels_on_dark" => {:bg => '#211e1e', :fg => '#dadada'},
20 "slush_poppies" => {:bg => '#f1f1f1', :fg => '#000000'},
21 "spacecadet" => {:bg => '#0d0d0d', :fg => '#dde6cf'},
22 "sunburst" => {:bg => '#000000', :fg => '#f8f8f8'},
23 "twilight" => {:bg => '#141414', :fg => '#f8f8f8'},
24 "zenburnesque" => {:bg => '#404040', :fg => '#dedede'}
25 }
26 FILE_TYPES = {'rb' => 'ruby', 'rjs' => 'ruby', 'rxml' => 'ruby_on_rails',
27 'rhtml' => 'ruby_on_rails', 'pl' => 'perl', 'pm' => 'perl',
28 'html' => 'html', 'applescript' => 'applescript',
29 'py' => 'python', 'js' => 'javascript', 'as' => 'actionscript',
30 'php' => 'php', 'c' => 'c', 'h' => 'c', 'java' => 'java',
31 'markdown' => 'markdown', 'sh' => 'shell-unix-generic',
32 'bashrc' => 'shell-unix-generic', 'sql' => 'sql',
33 'txt' => 'plain_text', 'textile' => 'textile',
34 'xml' => 'xml', 'tld' => 'xml', 'jsp' => 'xml',
35 'rss' => 'xml', 'yaml' => 'yaml', 'lisp' => 'lisp',
36 'hs' => 'haskell'
37 }
38
39 def initialize(file)
40 @extention = File.extname(file).delete('.')
41 @syntax = FILE_TYPES[@extention]
42 @data = IO.read(file)
43 end
44
45 Uv.themes.each do |theme|
46 define_method("parse_with_#{theme}") do
47 parse(theme, true)
48 end
49 end
50
51 def parse(theme, numbering)
52 html = Uv.parse( @data, "xhtml", @syntax, numbering, theme )
53
54 css_file = File.join(Uv.path, %w(render xhtml files css), "#{theme}.css")
55 styles = {}
56 IO.read(css_file).gsub(/pre\.#{theme}\s+\.(\w+)\s*\{(.+?)\}/m) do
57 styles[$1] = $2.gsub(/\s/, "")
58 end
59 styles.each do |key, value|
60 html.gsub!(/class="#{key}"/i, %Q{style="#{value}"})
61 end
62 html.gsub!(/class="#{theme}"/) { |match| match +
63 %Q{ style="background-color:#{BGCOLORS[theme][:bg]};color:#{BGCOLORS[theme][:fg]}"} }
64 end
65 end
66
67 if __FILE__ == $0
68 themes = Hilite::BGCOLORS.keys
69 ARGV.each do |file|
70 theme = themes[rand(themes.length)]
71 colored_code = Hilite.new(file).parse(theme, true)
72 puts "<p>#{file} with #{theme}</p>" + colored_code
73 end
74 # ARGV.each do |file|
75 # colored_code = Hilite.new(file).parse_with_cobalt
76 # puts "<p>#{file} with cobalt</p>" + colored_code
77 # end
78 end


使い方はターミナルで

  $ ruby hilite.rb fact.rb  > fact.html

とすれば
fact.rbを色付けしたコードを含むfact.htmlが得られる
引数として複数のファイルを指定できる
サンプルでは複数のコードに対して
ランダムにThemeを選択し出力している


69〜73行をコメントアウト
74〜77行のコメントを外せば
全てのコードをCobalt Themeで色付けしたものが得られる


Hiliteクラスの基本的な使い方は

  theme = 'cobalt'
  code = Hilite.new(filename)
  puts code.parse(theme, true)

ファイル名を引数としてHiliteクラスをオブジェクト化し
ThemeとNumberingの有無を引数としてparseメソッドを呼ぶ
あるいは

  code = Hilite.new(filename)
  puts code.parse_with_espresso_libre
  puts code.parse_with_blackboard

のようにThemeをwithしたメソッドを呼んでもいい


Syntaxはファイルの拡張子から
FILE_TYPESテーブルで一意に判断している
必要なSyntaxがテーブルにない場合は
適宜追加してほしい
使えるSyntaxはirb

irb> require 'uv'
irb> Uv.syntaxes

などとすれば得られる


ちなみにTextMateに必要なSyntax Bundleがない場合には
Manualの5.7 Getting More Bundles*5を参照にして
インストールしてほしい


何しろ素人が初めて投稿するコードなので
いろいろとまずい点があると思うけど
使ってくれる人がいたらうれしい


(追記:2009/3/27) コードは以下にアップしてあります。Shoesによるインタフェースもあります。

gist: 1451 - GitHub