ジェームス・クラーク式記法あれこれ
James Clark 's Notation
XML を記述する際にインデントを付加して読みやすく書いたつもりが思わぬ動作をすることがある.例えば以下の2つの文書は XML 的には同値ではない.
<foo><bar id='b1'><baz/><baz/></bar><bar id='b2'><baz>text</baz></bar></foo>
<foo> <bar id='b1'> <baz/> <baz/> </bar> <bar id='b2'> <baz>text</baz> </bar> </foo>
2つ目の文書は /foo と /foo/bar[1] の間に テキスト(改行1つとスペース2文字)が入っている.foo の子要素は5個になる.
これを,ジェームス・クラーク式記法で書いてみると以下のようになる.この記法の特徴はタグ名の前後の空白文字が無視されることを利用している.
<foo ><bar id='b1' ><baz /><baz /></bar ><bar id='b2' ><baz>text</baz ></bar ></foo >
これにスペースを付加すれば,2つ目の図でやりたかったインデントが実現できる.以下の例は1番目の1行版の XML 文書と同値である(上の例も当然同値).これでもう XML エディタとはおさらば?
<foo ><bar id='b1' ><baz /><baz /></bar ><bar id='b2' ><baz>text</baz ></bar ></foo >
ただ,これだと最後の部分が気持ち悪いので,次のように閉じタグを意識せずに書くのが個人的には好き.
#
<foo ><bar id='b1' ><baz /><baz/></bar ><bar id='b2' ><baz>text</baz></bar></foo>
James Clark's Notation への変換ツール
さて,このように大変素晴らしいジェームス・クラーク式記法であるが,今迄生成してきた XML 文書がもったいない.この記法に変換できるツールが無いものか.
見つけられなかったんで,Rubyに同梱されてるREXML のソースを眺めてみるとDocument#write() にオプション引数として,transitive なるモノがある.
# transitive:: # If transitive is true and indent is >= 0, then the output will be # pretty-printed in such a way that the added whitespace does not affect # the absolute *value* of the document -- that is, it leaves the value # and number of Text nodes in the document unchanged.
らしいんで,使ってみる.
% cat | ruby -rrexml/document -e 'REXML::Document.new(STDIN).write(STDOUT, 0, true)' <foo><bar id='b1'><baz/><baz/></bar><bar id='b2'><baz>text</baz></bar></foo> Ctrl-d <foo ><bar id='b1' ><baz /><baz /></bar ><bar id='b2' ><baz>text</baz ></bar ></foo > %
なかなかいい感じだけど,最後の改行が1つ多い?
REXML::Document#write がおかしいのか?
文書の最後にある改行文字の扱いが問題.Document の 子供ノードとして
@children.each { |node| indent( output, indent ) if node.node_type == :element - if node.write( output, indent, transitive, ie_hack ) - output << "\n" unless indent<0 or node == @children[-1] + if (node.write(output, indent, transitive, ie_hack) and + indent >= 0 and node != @children[-1]) + output << "\n" unless (node == @children[-2] and + node.next_sibling.to_s == "\n") end } end
なんだか根本的に直した方がいいと思うけど適当に直すとこんな感じになる.
# あと each のブロックも do ... end にしたいところ
# 元々のコードはtab-4 でインデントしてある
# インストールしてあるファイルをいじるとスクリプトファイルを持ち運ぶのが面倒なんで,スクリプト中で再定義
とりあえずこれでいい感じになった.でも個人的には閉じタグをつらつら書いてXMLの見た目が「>」な感じになるのは好きじゃないんで,さらに REXML::Element#write() をいじる
class Element def write(writer=$stdout, indent=-1, transitive=false, ie_hack=false) #print "ID:#{indent}" writer << "<#@expanded_name" @attributes.each_attribute do |attr| writer << " " attr.write( writer, indent ) end unless @attributes.empty? if @children.empty? if next_sibling if transitive and indent > -1 writer << "\n" indent(writer, indent - 1 ) writer << " " elsif ie_hack writer << " " end end writer << "/" elsif (transitive and indent > -1 and !@children[0].kind_of?(Text)) writer << "\n" indent(writer, indent + 1) end writer << ">" write_children(writer, indent, transitive, ie_hack) writer << "</#{expanded_name}" end if (transitive and indent>-1 and !@children.empty? and next_element) writer << "\n" indent -= 1 if next_sibling.nil? indent(writer, indent) end writer << ">" end end
もっときれいにならないものか.とりあえずこれで以下のような出力が得られる.
<foo ><bar id='b1' ><baz /><baz/></bar ><bar id='b2' ><baz>text</baz></bar></foo>