Tuesday, May 27, 2008

Super Simple XML-String to JSON

Yes, there are other libraries for this. Yes, they are more complete. So why this? I didn't need all the features of a complete xml->json map, I just wanted something to handle my nice, super simple use-case. And I hate wasted bandwidth. This library is 620 BYTES when YUI-compressed.

This is for a specific task. I trust the xml source. (So nobody yell "hey! - you're using eval() - you're eveel!").

Prerequisites:
  • you have a string of well-formed xml.
  • you don't have & don't care about node attributes, namespaces or CDATA (ie. your XML is 'Super Simple').

Good Ideas:
  • Don't use a library that will dirty the object prototype.
    (ie. mess up for-loops that look like this: for(var prop in obj){..})
  • Strip comments and whitespace, use safe variable-name substitution—use YUI Compressor.

Confessions:
  • It uses eval().
  • It uses regular-expressions (regexes).
  • It could probably stand to be optimized a bit.

The Code:

var xmlString = "<root><a/><b>data1</b><b>data2</b></root>";

function xmlString2json (xmlString) {
    return xmlString.replace(
            //expand empty tags
            /\<([^>]*)\/\>/g, "<$1></$1>"
        ).replace(
            //convert closing tags to closing braces
            /\<\/([^>]*)\>/g,"},"
        ).replace(
            //convert opening tags to "{NODENAME:["
            //notice that the array literal is opened, but not closed.
            /\<([^>]*)\>/g,"{'$1':[" 
        ).replace(
            //remove extraneous commas
            /,}/g,"]}"
        ).replace(
            //close array literal, begin adding needed singlequotes
            /:([^{}]*?)}/g,":'$1]'}"
        ).replace(
            //adjust singlequotes, bring inside array literal notation (left side)
            /'\[/g,"['"
        ).replace(
            //adjust singlequotes, bring inside array literal notation (right side)
            /\]'/g,"']"
        ).replace(
            //remove empty strings (which are from empty tags)
            /\[''\]/g,"[]"
        ).replace(
            //remove final, ending extraneous comma
            /},$/, '}'
    );
}

var json = xmlString2json(xmlString);
// {'root':[{'a':[]},{'b':['data1']},{'b':['data2']}]}

function cleanTree(r, p){
    if(r.length == 1 && typeof r[0] == "string"){
        //might want to trim leading and trailing whitespace from r[0] first
        // could be expressed as a series of ternary operations:
        // return r[0]=="true"?true:r[0]=="false"?false:r[0].search(/^[0-9]+$/)==0?1*r[0]:r[0]
        // expressed this way for readability:
        if(r[0] == "true"){
            return true;
        }
        if(r[0] == "false"){
            return false;
        }
        if(r[0].search(/^[0-9]+$/) == 0){
            return 1*r[0];
        }
        return r[0];
    }
    p = p || {};
    for( i in r ){
        var nn = '';
        for( name in r[i] ){
            nn = name;
        }
        var subnode = r[i][nn];
        if(p[nn]){
            if(typeof p[nn] == "object" && typeof p[nn].length == "number"){
                p[nn].push( cleanTree(subnode) );
            } else {
                p[nn] = [ p[nn], cleanTree(subnode) ];
            }
        } else {
            p[nn] = cleanTree(subnode);
        }
    }
    return p;
}

var jso = eval('('+json+')');
var cleaned = cleanTree(jso.testImages);
console.log(cleaned);


At this point, you might be wondering, "Why all those [extra] arrays?" My idea was to approximate the concept of the childNodes array. Really, you could just take the initial JSON and run with it, keeping in mind that it is structured with 'childNodes' arrays. But in case you'd rather have it 'cleaned' up .. I made the recursive cleanTree function to do just that.

"What if I have actual xml, not just a string representation thereof?" you may question.

Give this a shot:

var xmlString = (new XMLSerializer()).serializeToString( myXMLDoc );

Someone else's related post (reminder to self to see if his xml2json will work in actionscript / how readily adaptable it is): converting xml to json.

Saturday, April 5, 2008

consume that data

Have an array that you need to iterate over? Dying to use javascript 1.8 iterators and generators? (why?!) Anyhow... use this instead.. IF IF IF ... you don't mind CONSUMING your data :-) (this = shift)

var s=['prototype.yc','lightbox','dsn'];
while(s.length) document.write('<scr'+'ipt src="/js/'+s.shift()+'.js"></scr'+'ipt>');

Friday, April 4, 2008

directory structure = sitenav = breadcrumbs = CSS

Ok? Example:
/
home
home
home

/mainsection
mainsection
home > mainsection
<html class="mainsection">

/mainsection/subsection
mainsection > subsection
home > mainsection > subsection
<html class="mainsection"><body class="subsection">

/mainsection/subsection/leaf
mainsection > subsection > leaf
home > mainsection > subsection > leaf
<html class="mainsection"><body class="subsection"><div id="siteContainer" class="leaf">

Monday, March 31, 2008

dom nodes + for-loops THAT WORK

Imagine you have a collection of DOM nodes (html element nodes, whatever you want to call them) stored in the variable ‘elements’, and that var len = elements.length. The following for-loop will not work. Every element will have an onclick event handler function that calls clicked(len) – not the respective clicked(i)



WRONG

    for( var i = 0; i < len; i++){
        
        var el = elements[i];
        el.addListener('click', function(){clicked(i);}, false);
        
    }



RIGHT

    (function loop( I ){
        if (I == len) return;
        var i = I;
        
        
        var el = elements[i];
        el.addListener('click', function(){clicked(i);}, false);
        
        
        loop(++I);
    })(0);

Note: addListener is not any browser’s implementation. It’s just my way of saying addEventListener (or, for IE, attachEvent). Also, for brevity, the function ‘clicked’ is not here defined. Yes, this is documented in a couple places around the web. But those places are not obvious or easily searchable for everyone.

Tuesday, February 26, 2008

If

Have you ever seen code like this?

if(condition){
   stuff
}

This won't work for all cases, but I often enjoy writing it like this:

if(!condition) return;
stuff

Note: if you are NOT minifying your code, the bottom method is probably leaner, but if you ARE minifying, stick with the top method.

Reduce the Verbosity of Prototypal Class Definitions

I ran across some code like this recently:


function SomeClass(args){
...
}
SomeClass.prototype.firstMethod = function(args){ ... };
SomeClass.prototype.secondMethod = function(args){ ... };
SomeClass.prototype.thirdMethod = function(args){ ... };
...
...


Maybe, like me, you cringe when you see repetitious code. Maybe not. If not, return. So I got to thinking of a way to streamline this class.


function SomeClass(args){};
SomeClass.prototype = new (function(args){
   var me = this;
   me.firstMethod = function(args){ ... };
   me.secondMethod = function(args){ ... };
   me.thirdMethod = function(args){ ... };
   ...
})();

/* to test it, we'll create an instance and check if it has it's own [copy of] firstMethod */

var myClass = new SomeClass(args);
alert(myClass.hasOwnProperty('firstMethod'));
/* false, it does not have a local copy of the method */