ジェームス・クラーク式記法あれこれ

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 の 子供ノードとして... と REXML::Text("\n") が入ってる....を最後の要素と考えるなら期待通りに動作するが,今回の例では最後の改行が最後の要素になっている.よって,... に対する改行と,本来の文書末尾にある改行と併さって改行が2つ挿入されるハメになる.

      @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>

備考

  • この記法って「ジェームス・クラーク式記法」って名前なの?
  • 変換ツールないの?
  • transitive 指定で Document#write() の indent 引数を 1 以上にするとなんだか納得のいかない出力になる.indent 引数はインデント幅に使うスペースの数ではないの?
  • Element#write() をもちっとキレイにする.
  • でもやっぱり,XML手書きはアホっぽい(特に閉じタグを書くあたりが).SXMLとかyamlとか使って後でXML に変換するのが一番だと思う.