このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

ブラウザでミニマムXML (2):描く

とりあえずの第2回めー。なんか見出しが思いっきり短いですね。「描く」ってそれだけ。

ここでいう「描く」とは、XMLツリーの構造を見やすく視覚化することです。視覚化の対象となるDOMツリーは、もとのHTML文書の一部分だとしましょう。そして、視覚化の手段にブラウザのHTMLレンダリング機能を使います。

こういう視覚化ができないと、プログラムのテスト/デバッグがやりにくいので、まずは「描く」ことからはじめることにしたのです。

内容:

  1. DOMの知識はこれだけ
  2. 入れ子の箱で表現する
  3. 箱を作る関数を作る
  4. 要素を表現する箱
  5. 箱を実際に描く関数
  6. 試してみる
  7. 今回のまとめ

●DOMの知識はこれだけ

XMLやDOMの詳しい解説はしませんが、最小限のことをまとめておきます。今回使うプロパティ/メソッドは以下のものです。型の記述にはJava風の記法を使っています。(今回の説明の都合上、Nodeは使わず代わりにElementOrTextと書いています。)

名前と引数型 値の型 使えるクラス
createElement(String name) Element Document
createTextNode(String data) Text Document
appendChild(ElememtOrText child) ElementOrText Element
setAttribute(String name, String value) void Element
childNodes ElementOrText[] Element
className String Element
nodeType Number ElementOrText

DOMでは、テキストデータは文字列(string)ではなくて、文字列を内部に保持するテキストノード(Textクラスのインスタンス、より正確にはTextインターフェースを持つオブジェクト)で表現されることに注意しましょう。

要素ノードオブジェクトのclassNameプロパティは、HTMLのclass属性と同じ意味ですが、ブラウザはclass属性よりclassNameプロパティに反応するようです。nodeTypeは、ノードの種類判別に使えて、値が1なら要素ノード、3ならテキストノードです。

これ以上の詳しいことは他の資料で調べてください。

入れ子の箱で表現する

ツリー構造の視覚化には、入れ子の箱を使います。親ノードが外の箱、子ノードはその中に詰め込まれた箱になります。

要素の箱(element box)のレイアウトは次のようにしましょう。

+---(element box)---+
|+---------------+|
||tag name ||
|+---------------+|
|+---------------+|
||child #1 ||
|+---------------+|
|+---------------+|
||child #2 ||
|+---------------+|
・ … ・
・ … ・
|+---------------+|
+-------------------+

テキストの箱はもっと単純で、次のようです。

+---(text box)----+
| text data |
+-----------------+

これらの箱は、HTMLのdiv要素で表現します。

要素の属性や、その他のノード(コメント、PI、CDATAセクション、実体参照)は、今回は省略します。

●箱を作る関数を作る

方針が決まったので、実装しましょう。以下では、効率が悪くても冗長でも、JavaScriptプログラムを小さな関数に分割してちょっとずつ作っていくことにします。ここでは、関数ライブラリ・スタイルで作っていきますが、クラス風にまとめたコードは別エントリーで紹介します。

まずは箱(div要素)を作る関数:


function createBox(className) {
// className:string
// return:Element

var box = document.createElement('div');
box.setAttribute('class', className);
box.className = className;
return box;
}

箱の種類はclass属性に記録されます。属性classとプロパティclassNameがいつでも同期している保証がないので、明示的に、両方を同じ値に設定しています。

このcreateBoxを使って、テキストノードに対応する箱を作る関数createTextBoxを定義してみます。


function createTextBox(data) {
// data:string
// return:Element

// 素材を準備
var textBox = createBox('textBox');
var text = document.createTextNode(data);
// 組み立てる
textBox.appendChild(text);
// 戻す
return textBox;
}

createTextBox("hello")と呼び出すと、次のようなHTML要素ができます。


<div class="textBox">hello</div>

class属性値の'textBox'を目安に、CSSにより見た目を制御できます。たとえば、箱内の文字を茶色い(maroon)太字にしたいなら:


.texBox {color:maroon; font-weight:bold }

●要素を表現する箱

要素の箱のほうはもう少し複雑になります。


function createElementBox(name) {
// name:string
// return:Element

// 素材を準備
var elementBox = elementBox = createBox('elementBox');
var tagNameBox = createBox('tagNameBox');
var tagNameText = document.createTextNode("<" + name + ">");
// 組み立てる
tagNameBox.appendChild(tagNameText);
elementBox.appendChild(tagNameBox);
// 戻す
return elementBox;
}

createElementBox("foo")と呼び出すと、次の要素が返ります。


<!-- インデントは見やすさのために付けた -->
<div class="elementBox">
<div class="tagNameBox">&lt;foo></div>
</div>

この定義でもいいのですが、箱の表示の際に、最上位の要素だけ見た目を変えたいときがあるので、以下のように直しておきます。第2引数が指定されていてtrueとみなせる値ならば、種類がrootBoxである箱が返ってきます。


function createElementBox(name, isRoot_) {
// name:string
// isRoot_:any as boolean

// 素材を準備
var elementBox;
if (isRoot_) {
elementBox = createBox('rootBox');
} else {
elementBox = createBox('elementBox');
}
var tagNameBox = createBox('tagNameBox');
var tagNameText = document.createTextNode("<" + name + ">");
// 組み立てる
tagNameBox.appendChild(tagNameText);
elementBox.appendChild(tagNameBox);
// 戻す
return elementBox;
}

●箱を実際に描く関数

次は箱を描く関数です。まずは簡単な、テキストの箱を描く関数:


function drawText(canvas, textNode) {
// canvas:Element
// textNode:Text
// return:void

var box = createTextBox(textNode.data);
canvas.appendChild(box);
}

ここで、canvasってのは、箱を描く場所を提供するHTML要素です。canvasもHTMLのdivであることを(暗黙に)仮定しています。canvasに箱(これもdiv要素)を挿入すると、ブラウザの再レンダリングが起きて実際に描かれることになります。

次は要素を描く関数です。第3引数によりルートかどうかを振り分けます。


function drawElement(canvas, elmNode, isRoot_) {
// canvas:Element
// elmNode:Element
// isRoot_:any as boolean
// return:void

// element node itself
var box = createElementBox(elmNode.tagName, isRoot_);
canvas.appendChild(box);
// child nodes
var children = elmNode.childNodes;
for (var i = 0; i < children.length; i++) {
switch(children[i].nodeType) {
case 1: // ELEMENT_NODE
drawElement(box, children[i]);
break;
case 3: // TEXT_NODE
drawText(box, children[i]);
break;
default:
// do nothing
break;
}
}
}

drawElementのなかで、もう一度drawElement(またはdrawText)を呼んでます。そう、再帰処理ですね。

●試してみる

ここまでくれば、実際に描くことができます。次の例のような構造を描いてみます。


<div id="target">
<h2>サンプル</h2>
<p>こんにちは。</p>
<p>これは<em>サンプル</em>です。</p>
</div>

今までに作った関数を1つのファイルdrawxml-1.jsにまとめた上で、これをライブラリとして使い、次のスクリプトを含んだHTMLファイルを作ります。


<script src="drawxml-1.js"></script>
<script>
function draw() {
var canvas = document.getElementById("canvas");
var target = document.getElementById("target");
drawElement(canvas, target, true);
}
</script>

あとは、textBox, rootBox, elementBox, tagNameBoxというHTML/CSSクラスに対して、適当にスタイルを定義して見た目を調整します。draw()を呼び出すボタンも必要ですね。

●今回のまとめ

今回の視覚化では、属性などを無視しているし、キャンバスのクリアができないなど機能的にも不足があります。ですが、それらは必要になったら補うことにします。とりあえず、要素/テキストのツリー構造を描くことはできるようになりました。

  1. 数個のDOM APIメソッド/プロパティの知識で「描く」操作ができる。
  2. ツリー構造の視覚化には、入れ子の箱が手軽で分かりやすい。
  3. 「箱」は、class属性を指定したdiv要素で実現する。
  4. HTML文書ツリーに「箱」を挿入するだけで、その「箱」を描ける。