RubyでHTML入力支援

発端

HTMLを作成する必要が生じて昔の知識を思い出しながらやっているのだけれど、どうも思考と入力がちぐはぐになってしまう。なにが思考を妨げているかというと、1.大量のカッコとクオートを使うので、シフトキーを多用する、2.閉じのタグが画面をごちゃごちゃにしてしまう、の2点。
ただ、HTMLのDOM構造から離れるわけにはいかないので、ひとまずはインデントでタグのネストを表し、それを変換するようなスクリプトを書けばいいのでは? という考えに到達した。
つまり、

table
  tr
    td あ
    td い
  tr
    td う
    td え

を(上の例ではスペースでインデントしてます)

<table>
  <tr>
    <td></td>
    <td></td>
  </tr>
  <tr>
    <td></td>
    <td></td>
  </tr>
</table>

に変換してくれるような。

構想、仕様の策定

作りながら固まっていった最終的な仕様は、

  • #で始まる行・・・コメント
  • !で始まる行・・・その行をそのまま出力(直前までの閉じてないタグを全て閉じる)
  • *で始まる行・・・その行をそのまま出力(ネストに影響しない)
  • 1行の要素 各要素は基本的に半角スペースで区切る
    • ネストはタブ(\t)の個数で表す
    • タブのあとの最初の文字列はタグ名
    • id #で始まる文字列
    • クラス名 .で始まる文字列
    • 一般的なタグの属性は、プロパティ=値 
    • タグに含まれる文字列 もう一度タブ(\t)で区切り、その後に記述

実装

見事な一本スクリプト。後悔はしてない。それなりに役に立っている。
実装を念頭において仕様を決めたのでかなり楽だった。

if ARGV[0]==nil
	puts "error: no input file"
	exit(0)
end
close_tags = Array.new
output_filename = ARGV[1]==nil ? ARGV[0]+"_out.html" : ARGV[1]
input = File.open(ARGV[0])
output = File.open(output_filename, "w")

input.each_line{|line|
	if line[0].chr=="!"
		close_tags.reverse_each{|elem|
			output.puts elem['tag']
			close_tags.pop
		}
		output.puts line.sub(/^!/, "")
		next
	elsif line[0].chr=="*" 
		output.puts line.sub(/^\*/, "")
		next
	elsif line[0].chr=="#" || line.length==1
		next
	end
	output_html = "<"
	class_names = ""
	id = ""
	line.sub!(/^[	]+/, "")#タブ(\t)を検索している
	nest = $&==nil ? 0 : $&.length
	
	close_tags.reverse_each{|elem|
		break if nest>elem['nest']
		output.puts elem['tag']
		close_tags.pop
	}
	ar = line.split("\t")
	ar[1] = "" if ar[1]==nil
	output_html += ar[0].split(" ")[0]#半角スペースで分割している
	ar[0].split(" ").each{|e|
		if e=~/^#/
			id = e.sub(/^#/,"")
			output_html += " id=\""+id+"\""
		elsif e=~/^\./
			class_names += " "+e.sub(/\./, "")
		elsif e=~/=/
			params = e.split("=")
			params[1] = params[1]==nil ? "" : params[1]
			output_html += " "+params[0]+"=\""+params[1]+"\""
		end
	}
	output_html += " class=\""+class_names.strip+"\"" if class_names.length > 0
	output.print output_html += ">"+ar[1].strip
	close_tags << {'nest'=>nest, 'tag'=>"</"+ar[0].split(" ")[0]+">\n"}
}
close_tags.reverse_each{|elem|
	output.puts elem['tag']
	close_tags.pop
}
input.close
output.close

特徴

  • 文字列の置換を一切行わないので、タグを書くのも自由。
  • imgやbrなどを考慮していない。
  • ただし、行頭の*を使うことでそのままタグを出力できるのでうまくやってください。

実行例

!<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
html xmlns=http://www.w3.org/1999/xhtml
	head
*<meta content="text/html" charset="utf-8" http-equiv="Content-Type"/>
		title	タイトル
	body
		a #examplelink onclick=func1();	リンク
		table border=1 .exampleclass
			tr
				td	これ
				td	は例
				td	です
		div .class1 .class2 #id0
			b	太字にした
		div .class 1	<i>斜体にした</i>

出力

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head><meta content="text/html" charset="utf-8" http-equiv="Content-Type"/>
<title>タイトル</title>
</head>
<body><a id="examplelink" onclick="func1();">リンク</a>
<table border="1" class="exampleclass"><tr><td>これ</td>
<td>は例</td>
<td>です</td>
</tr>
</table>
<div id="id0" class="class1 class2"><b>太字にした</b>
</div>
<div class="class"><i>斜体にした</i></div>
</body>
</html>

出力されたHTMLの整形がひどい。ただ、余計なスペースや改行がタグの中に入らないように一応気を遣っている。

補足

bodyなどの広域なタグもこの記法で書くと、それに含まれるタグのネストが深くなってしまう(タブ(\t)を何回も打たないといけない)ので、bodyやheadなど、あまりいじらなさそうなところは行頭「!」でそのまま出力したらいいかと。

2011/07/20追記

Zen-Codingというのがあることを最近知った。