uhyohyo.net

JavaScript初級者から中級者になろう

六章第三回 XHTMLとDOM

今回は、DOMでXHTMLを扱うときの方法について解説します。とはいっても基本はHTMLと同じなので、専らXHTMLというよりXMLに特有の扱い方を解説することになります。

タグ名の大文字と小文字

HTMLではノードのtagNameを見たときそのタグ名は大文字でしたが、XHTMLになると違います。前解説したように、XHTML(XML)ではタグ名の大文字と小文字は区別され、XHTMLのタグ名は小文字に統一されています。つまり、tagNameなどを見たときも小文字になるということです。

<?xml version="1.0" ?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta charset="UTF-8" />
    <title>test</title>
  </head>
  <body>
    <p>test</p>

    <script type="text/javascript">
      <![CDATA[
        console.log(document.documentElement.tagName);
      ]]></script>
  </body>
</html>
          

これを実行すると、小文字で"html"と表示されます。document.documentElementは三章第五回ででてきたものでその文書のルート要素、つまりXHTMLの場合はhtml要素でした。そのタグ名というわけです。

XHTMLのサンプルを実行できない場合、拡張子を「htm」「html」でなく「xhtml」にするとうまくいくかもしれません。

ノードと名前空間

名前空間の情報を得る

次の場合を見てみましょう。

<?xml version="1.0" ?>
<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <xhtml:head>
    <xhtml:meta charset="UTF-8" />
    <xhtml:title>test</xhtml:title>
  </xhtml:head>
  <xhtml:body>
    <xhtml:p>test</xhtml:p>

    <xhtml:script type="text/javascript">
      <![CDATA[
        console.log(document.documentElement.tagName);
      ]]></xhtml:script>
  </xhtml:body>
</xhtml:html>
          

デフォルト名前空間を使用せずに、名前空間接頭辞にxhtmlを使って書いた形です。

このとき、html要素のtagNameは"xhtml:html"となっています。つまり、tagNameには名前空間接頭辞も含んだタグ名が入っているということです。

ちなみにこのように名前空間接頭辞も含んだタグ名は「qualified name」の略で「QName」とよばれます。

名前空間接頭辞と、それ以外を別々に取得する方法もあります。

<?xml version="1.0" ?>
<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <xhtml:head>
    <xhtml:meta charset="UTF-8" />
    <xhtml:title>test</xhtml:title>
  </xhtml:head>
  <xhtml:body>
    <xhtml:p>test</xhtml:p>

    <xhtml:script type="text/javascript">
      <![CDATA[
        console.log(document.documentElement.prefix);
        console.log(document.documentElement.localName);
      ]]></xhtml:script>
  </xhtml:body>
</xhtml:html>
          

prefixとlocalNameという2つのプロパティが出てきました。prefixが接頭辞、localNameがローカル名(接頭辞を含まないタグ名)です。

また、名前空間の実体は接頭辞ではなくURI、例えばXHTMLの場合はhttp://www.w3.org/1999/xhtmlでした。こちらを取得したいという場合もあると思います。むしろ、同じ名前空間でも名前空間接頭辞は自由に決められるので、こちらを取得したい場合のほうが多いのではないでしょうか。それもちゃんとできます。

<?xml version="1.0" ?>
<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <xhtml:head>
    <xhtml:meta charset="UTF-8" />
    <xhtml:title>test</xhtml:title>
  </xhtml:head>
  <xhtml:body>
    <xhtml:p>test</xhtml:p>

    <xhtml:script type="text/javascript">
      <![CDATA[
        console.log(document.documentElement.namespaceURI);
      ]]></xhtml:script>
  </xhtml:body>
</xhtml:html>
          

このnamespaceURIに名前空間のURIが入っています。「namespace」が名前空間のことです。

名前空間を持ったノードを作る

さて、上のnamespaceURIは書き換えられません。だから、あるノードを作るとき、そのノードが属する名前空間は作るときに決めることが必要になります。

そのためのものがあります。

<?xml version="1.0" ?>
<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <xhtml:head>
    <xhtml:meta charset="UTF-8" />
    <xhtml:title>test</xhtml:title>
  </xhtml:head>
  <xhtml:body>
    <xhtml:p>test</xhtml:p>

    <xhtml:script type="text/javascript"><![CDATA[
      var newp = document.createElementNS("http://www.w3.org/1999/xhtml", "xhtml:p");

      newp.appendChild(document.createTextNode("newp"));

      document.body.appendChild(newp);

      console.log(newp.namespaceURI);
    ]]></xhtml:script>
  </xhtml:body>
</xhtml:html>
          

ここで、名前空間を指定して要素のノードを作っているのはcreateElementNSというメソッドです。createElement(二章第五回)の名前空間あり版です。

引数も2つに増えていて、1つめが名前空間のURI、2つめが接頭辞つきのタグ名(QName)です。

これで名前空間に属するノードができます。最後にconsole.logでnamespaceURIを表示していますが、ちゃんと"http://www.w3.org/1999/xhtml"が表示されます。

ちなみに、createElementNSの次にテキストノードを作って追加していますが、テキストノードは特に名前空間に属したりはしないので、特に変わりません。

また、接頭辞は新しく要素を作るときにはあまり関係ありません。

<?xml version="1.0" ?>
<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <xhtml:head>
    <xhtml:meta charset="UTF-8" />
    <xhtml:title>test</xhtml:title>
  </xhtml:head>
  <xhtml:body>
    <xhtml:p>test</xhtml:p>

    <xhtml:script type="text/javascript"><![CDATA[
      var newp = document.createElementNS("http://www.w3.org/1999/xhtml", "aaaaaaa:p");

      newp.appendChild(document.createTextNode("newp"));

      document.body.appendChild(newp);

      console.log(newp.namespaceURI);
    ]]></xhtml:script>
  </xhtml:body>
</xhtml:html>

のようにしても正しくXHTMLの名前空間に属します。

だから、事実上接頭辞は省略できます。第二引数はただ単に"p"としても一応問題ないわけです。

また、名前空間を含む文書で普通のcreateElementを使用した場合、名前空間に属しません。そのため、XHTML文書でcreateElementNSではなくcreateElementを使うと、XHTMLの語彙には含まれない謎の要素が生成されたことになり、期待する動作をしてくれません。注意しましょう。

getElementsByTagNameNS

getElementsByTagNameの名前空間対応版もあります。それがgetElementsByTagNameNSです。これが威力を発揮するのは、例えばXHTMLに他の名前空間の語彙を埋め込んだ(使った)場合など、

<?xml version="1.0" ?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:abc="http://example.com/abc">
  <head>
    <title>test</title>
  </head>
  <body>
    <p><a href="http://google.com/">Go<abc:a>ogl</abc:a>e</a></p>
    <p><a href="http://google.com/"><abc:a>Goo</abc:a>gle</a></p>
  </body>
</html>
          

この中からリンクのa(XHTMLのa)だけを抜き出したい場合に威力を発揮したりします。

例えば、次のようにします。

document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "a")

第一引数が名前空間で、第二引数がタグ名です。これで、指定したタグのち、その名前空間に属するものだけを取得できます。

注意するのが、createElementNSのときはタグ名がQNameだったのに対し、今回はローカル名(接頭辞がつかない名前)です。

また、名前空間は関係なく、とにかくaなら全て抜き出したいという場合もあるかもしれません。そういう場合普通のgetElementsByTagNameを使えばいいように思えますが、実は名前空間がある文書の場合、getElementsByTagNameはどんな動作をするか決まっていません。すなわち、名前空間がある文書の場合はgetElementsByTagNameは使うべきではないのです。

それではどうすればいいかというと、getElementsByTagNameNSの第一引数に"*"を指定すると、全ての名前空間から探すことができます。

属性の名前空間

前回解説しませんでしたが、実は属性も名前空間を持ちます。

つまり、例えば

<abc:a abc:aaa="aaaaaa" />

という感じですね。このabcも名前空間接頭辞で、タグ名に使うのとまったく同じです。

さて、そうはいっても、タグ名と属性では少し違う部分があります。

タグ名に接頭辞がつかない場合はデフォルト名前空間に属することになっていましたが、属性の場合そうではありません。そういう場合、属性は名前空間に属しません。その属性は「要素に属する」ということになっています。

この要素に属しているという関係は、HTMLのときのそれと同じです。HTMLの属性も要素に属しているものでした。要素に属しているから、その要素が名前空間に属しているから自分自身は属していなくても問題ないのです。

それに対し、属性に接頭辞をつけた場合、その属性はグローバル属性というものになります。この属性は、「要素から独立している」のだそうです。一応要素とくっついていますが、要素に属しているというわけではないらしいです。例えば、

<xhtml:a href="http://google.com/" abc:aaa="aaaa">

のような場合に、このa要素はxhtmlの名前空間に属する要素です。hrefは名前空間に属していないから、このa要素に属していることになりますが、対してabc:aaa属性は、別の名前空間のabcに属する属性ということになります。

このときこのaaaはグローバル属性です。XHTMLで書かれた文書に何か独自の情報を追加したいとき、このように別の名前空間の語彙を要素の属性としてくっつけておくことができます。

もともと属する名前空間が違うから、このa要素はaaaなんて属性は知りません。だから、aaaがa要素に属しているというわけではないのです。そういう意味で、このaaaは独立しているといえます。

これらの属性は、JavaScriptでの扱い方が違います。グローバル属性でない場合(要素に属した属性の場合)、HTMLの名前空間が無い場合と同じだから、今まで同様getAttributeとsetAttribute(二章第十回)で操作できます。

対して、グローバル属性は、名前空間に属しているから扱い方が違います。

<a aaa:abc="..." bbb:abc="..." />

のような場合に区別する必要があるからですね。

ここで使うのが、getAttributeNSsetAttributeNSです。

まず、getAttributeNSは引数が2つになりました。

<?xml version="1.0" ?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:abc="http://example.com/abc">
  <head>
    <title>test</title>
  </head>
  <body abc:aaa="ababababab">
    <p>test</p>

    <script type="text/javascript"><![CDATA[
      console.log(document.body.getAttributeNS("http://example.com/abc", "aaa") );
    ]]></script>
  </body>
</html>
          

第一引数は名前空間のURIで、第二引数が属性のローカル名(接頭辞がつかない名前)です。

setAttributeNSも引数が1つ増えます。

<?xml version="1.0" ?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:abc="http://example.com/abc">
  <head>
    <title>test</title>
  </head>
  <body abc:aaa="ababababab">
    <p>test</p>

    <script type="text/javascript"><![CDATA[
      document.body.setAttributeNS("http://example.com/abc", "abc:aaa", "cccccc");
    ]]></script>
  </body>
</html>
          

第一引数が名前空間、第二引数が属性名、第三引数が変更後の値ということになります。

ここで注意するのが、getAttributeNSではローカル名だった第二引数が、setAttributeNSではQName(接頭辞も含む名前)になっていることです。とはいっても、createElementNSのそれと同じようにローカル名だけでも動作はします。

以上でXHTML・XMLに関する解説は終了です。ここで解説しなかったことに関しては、だいたいはHTMLのノウハウが通用します(というより、DOMとはそもそもXMLを操作するためのものであって、それをHTMLにも適用しているのだということもできます)。