JSON形式でHTMLのDOMツリーを書く

JSON形式でツリー状のオブジェクトとか書いていると、HTMLもこれでいいじゃんっていう気になった。例えばこんな感じ。

{
    tagName: "table",
    body: {
        tagName: "tbody",
        body: {
            tagName: "tr",
            style: "font-size:small",
            body: [
                { tagName: "td", body: "一列目", style: "text-align:right" },
                { tagName: "td", body: "2列目" },
                { tagName: "td", body: "3列目" },
                { tagName: "td", body: ["ヨン列目", "マジで"] }
            ]
        }
    }
}

これをHTMLの

<table>
    <tbody>
        <tr style="font-size:small">
            <td style="text-align:right">一列目</td>
            <td>2列目</td>
            <td>3列目</td>
            <td>ヨン列目マジで</td>
        </tr>
    </tbody>
</table>

というようなDOMに変換。タグの名前をtagNameプロパティ、をbodyに書いて、それ以外の各オブジェクトのプロパティは全部要素のプロパティにしちまえっていう話です。

以下のようなテストケースをパスすれば、一応できたっぽいと言えると思ってjsUnitで書いてみたっす。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
            "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">  
<title>Test loading a local HTML Document</title>
<link rel="stylesheet"  type="text/css" href="../css/jsUnitStyle.css">
<script language="JavaScript" type="text/javascript" src="jsUnitCore.js"></script>
<script language="JavaScript" type="text/javascript" src="../js/prototype.js"></script>
<script language="JavaScript" type="text/javascript" src="../js/brownie-json-dom.js"></script>
<script language="JavaScript" type="text/javascript">
<!--
function test_BuildTable() {
    var builder = new JsonDomBuilder("body", document);
    var tableElement = builder.execute( 
        {
            tagName: "table",
            body: {
                tagName: "tbody",
                body: {
                    tagName: "tr",
                    style: "font-size:small",
                    body: [
                        { tagName: "td", body: "一列目", style: "text-align:right" },
                        { tagName: "td", body: "2列目" },
                        { tagName: "td", body: "3列目" },
                        { tagName: "td", body: ["ヨン列目", "マジで"] }
                    ]
                }
            }
        }
    );
    assertEquals("TABLE", tableElement.tagName);
    assertEquals(1, tableElement.childNodes.length);
    var tbody = tableElement.childNodes[0];
    assertEquals("TBODY", tbody.tagName);
    assertEquals(1, tbody.childNodes.length);
    var tr1 = tbody.childNodes[0];
    assertEquals("TR", tr1.tagName);
    assertEquals("small", tr1.style.fontSize);
    assertEquals(4, tr1.childNodes.length);
    var td1 = tr1.childNodes[0];
    var td2 = tr1.childNodes[1];
    var td3 = tr1.childNodes[2];
    var td4 = tr1.childNodes[3];
    assertEquals("TD", td1.tagName);
    assertEquals("right", td1.style.textAlign);
    assertEquals(1, td1.childNodes.length);
    assertEquals("#text", td1.childNodes[0].nodeName);
    assertEquals("一列目", td1.childNodes[0].nodeValue);
    
    assertEquals("TD", td2.tagName);
    assertEquals(1, td2.childNodes.length);
    assertEquals("#text", td2.childNodes[0].nodeName);
    assertEquals("2列目", td2.childNodes[0].nodeValue);
    
    assertEquals("TD", td3.tagName);
    assertEquals(1, td3.childNodes.length);
    assertEquals("#text", td3.childNodes[0].nodeName);
    assertEquals("3列目", td3.childNodes[0].nodeValue);
    
    assertEquals("TD", td4.tagName);
    assertEquals(2, td4.childNodes.length);
    assertEquals("#text", td4.childNodes[0].nodeName);
    assertEquals("ヨン列目", td4.childNodes[0].nodeValue);
    assertEquals("#text", td4.childNodes[1].nodeName);
    assertEquals("マジで", td4.childNodes[1].nodeValue);
}
--></script>
</head>
<body>
<h1>JSON-DOM Tests</h1>
<p>This page tests loading data documents asynchronously.  To see them, take a look at the source.</p>

</body>
</html>


で、これをパスするクラスはこんな感じで作ってみた。

/**
 * brownie-json-dom.js
 * 
 * uses 
 *  prototype.js
 *
 * @author T.Akima
 * @copyright T.Akima
 * @license LGPL
 */
JsonDomBuilder = Class.create();
JsonDomBuilder.prototype = {
    initialize: function( bodyPropertyName,  baseDocument ) {
        this.bodyPropertyName = bodyPropertyName || "body";
        this.baseDocument = baseDocument || document;
    },
    
    execute: function( obj ) {
        return this.dispatchBuild(obj);
    },
    
    dispatchBuild: function( obj ) {
        if (obj.constructor == Array) {
            return this.buildNodes( obj );
        } else if (obj.tagName) {
            return this.buildNode( obj );
        } else if (obj.constructor == String) {
            return this.buildText( obj );
        } else {
            //throw new Error("obj  have no tagName ");
            return this.buildText( obj.toString() );
        }
    },
    
    buildText: function( string ) {
        return this.baseDocument.createTextNode( string );
    },
    
    buildNode: function( obj ) {
        var result = this.baseDocument.createElement( obj.tagName );
        this.applyAttributes( result, obj, ["tagName", this.bodyPropertyName] );
        var body = obj[ this.bodyPropertyName ];
        if (body) {
            var children = this.dispatchBuild( body );
            if (children) {
                if (children.constructor != Array) 
                    children = [ children ];
                for( var i = 0; i < children.length; i++ ) {
                    result.appendChild( children[i] );
                }
            }
        }
        return result;
    },
    
    buildNodes: function( arrayObj ) {
        var result = new Array();
        for(var i = 0; i < arrayObj.length; i++) {
            if (arrayObj[i])
                result.push( this.dispatchBuild( arrayObj[i] ) );
        }
        return result;
    },
    
    applyAttributes: function( node, attributeObj, ignoreProperties ) {
        if (!attributeObj)
            return;
        for(var prop in attributeObj) {
            if (ignoreProperties && this._array_contains( ignoreProperties, prop ))
                continue;
            if (prop == "style" &&  (navigator.appVersion.indexOf("MSIE") > -1)) {
                var style = attributeObj[prop];
                var styleObj = Style.toStyleObject( style );
                for(var styleItemName in styleObj)
                    node.style[styleItemName] = styleObj[styleItemName];
                continue;
            } else if (prop == "className" || prop == "class") {
                node.className = attributeObj[prop];
            } else {
                node.setAttribute(prop, attributeObj[prop]);
            }
        }
    },
    
    _array_contains: function( arrayObj, obj ) {
        for(var i = 0; i < arrayObj.length; i++) {
            if (arrayObj[i] == obj)
                return true;
        }
        return false;
    }
}
Style = {
    /**
     * "text-align:right; display:block; " というようなstyleの文字列を
     * { textAlign: "right", display: "block" } という オブジェクトに直す。
     * IEがsetAttributeでstyleを設定できないから必要になった。
     */
    toStyleObject: function( styleString ) {
        if (!styleString)
            return null;
        var result = {};
        var entries = styleString.split( ";" );
        for(var i = 0; i < entries.length; i++) {
            var items = entries[i].split( ":", 2 );
            if (items.length < 1)
                continue;
            var key = Style.toJsStyleName( items[0] );
            var value = items.length < 2 ? null : items[1];
            result[key] = value;
        }
        return result;
    },

    /**
     * text-align -> textAlign
     * というような変換を行う。
     */
    toJsStyleName: function( cssStyleName, delimeter ) {
        delimeter = delimeter || "-";
        if (!cssStyleName)
            return null;
        var items = cssStyleName.split( delimeter );
        if (items.length < 1)
            return "";
        var result = items[0];
        for(var i = 1; i < items.length; i++) {
            var s = items[i];
            result = result + s.charAt(0).toUpperCase() + s.substring(1).toLowerCase();
        }
        return result;
    }
}

executeメソッドの戻り値のelementは、どのelementにもappendChildされてないので、どこかにappendChildしてやればそれなりに表示されるっていう寸法です。

一応、IE6とFirefox1.0.7でパスしたのを確認しましたが、まあきっと何か抜けてんじゃないかなと思って、ツッコミを頂くために公開してみたわけです。何か気付いたら教えてくださいませ。

・・・と、ここまで書いて今更、もう誰か作ってんじゃねーの?とか思った。既にこんなのがあったら教えてください・・・・