UNPKG

jsxml

Version:

javascript utilities for working xml, mainly in jsonml in array form

320 lines (294 loc) 9.89 kB
// # JsonML // // Various functions for handling // jsonml in array form. // For more info on jsonml, // see [jsonml.org](http://jsonml.org/) // or [wikipedia](http://en.wikipedia.org/wiki/JsonML) // // Implemented to be as portable as possible. // Not depending on any libraries, and also // avoid regular expressions to be possible to // run on javascript-subsets on j2me devices. // // ## XML parser // // Parse an XML-string. // Actually this is not a full implementation, but just // the basic parts to get it up running. // Nonetheless it is Good Enough(tm) for most uses. // // Known deficiencies: CDATA is not supported, will accept even // non-well-formed documents, <?... > <!... > are not really handled, ... exports.fromXml = function(xml) { if(typeof(xml) !== "string") { JsonML_Error( "Error: jsonml.parseXML didn't receive a string as parameter"); } // white space definition var whitespace = " \n\r\t"; // the current char in the string that is being parsed var c = xml[0]; // the position in the string var pos = 0; // stack for handling nested tags var stack = []; // current tag being parsed var tag = []; // read the next char from the string function next_char() { c = ++pos < xml.length ? xml[pos] : undefined; } // check if the current char is one of those in the string parameter function is_a(str) { return str.indexOf(c) !== -1; } // return the string from the current position to right before the first // occurence of any of symb. Translate escaped xml entities to their value // on the fly. function read_until(symb) { var buffer = []; while(c && !is_a(symb)) { if(c === '&') { next_char(); var entity = read_until(';'); if(entity[0] === '#') { if(entity[1] === 'x') { c = String.fromCharCode(parseInt(entity.slice(2), 16)); } else { c = String.fromCharCode(parseInt(entity.slice(1), 10)); } } else { c = entities[entity]; if(!c) { JsonML_Error("error: unrecognisable xml entity: " + entity); } } } buffer.push(c); next_char(); } return buffer.join(""); } // The actual parsing while(is_a(whitespace)) { next_char(); } while(c) { if(is_a("<")) { next_char(); // `<?xml ... >`, `<!-- -->` or similar - skip these if(is_a("?!")) { if(xml.slice(pos, pos+3) === "!--") { pos += 3; while(xml.slice(pos, pos+2) !== "--") { ++pos; } } read_until('>'); next_char(); // `<sometag ...>` - handle begin tag } else if(!is_a("/")) { // read tag name var newtag = [read_until(whitespace+">/")]; // read attributes var attributes = {}; var has_attributes = 0; while(c && is_a(whitespace)) { next_char(); } while(c && !is_a(">/")) { has_attributes = 1; var attr = read_until(whitespace + "=>"); if(c === "=") { next_char(); var value_terminator = whitespace+">/"; if(is_a('"\'')) { value_terminator = c; next_char(); } attributes[attr] = read_until(value_terminator); if(is_a('"\'')) { next_char(); } } else { JsonML_Error("something not attribute in tag"); } while(c && is_a(whitespace)) { next_char(); } } if(has_attributes) { newtag.push(attributes); } // end of tag, is it `<.../>` or `<...>` if(is_a("/")) { next_char(); if(!is_a(">")) { JsonML_Error('expected ">" after "/" within tag'); } tag.push(newtag); } else { stack.push(tag); tag = newtag; } next_char(); // `</something>` - handle end tag } else { next_char(); if(read_until(">") !== tag[0]) { JsonML_Error("end tag not matching: " + tag[0]); } next_char(); var parent_tag = stack.pop(); if(tag.length <= 2 && !isArray(tag[1]) && typeof(tag[1]) !== "string") { tag.push(""); } parent_tag.push(tag); tag = parent_tag; } // actual content / data between tags } else { tag.push(read_until("<")); } } return tag; }; // ## XML generation // Convert jsonml in array form to xml. exports.toXml = function(jsonml) { var acc = []; toXmlAcc(jsonml, acc); return acc.join(''); }; // The actual implementation. As the XML-string is built by appending to the // `acc`umulator. function toXmlAcc(jsonml, acc) { if(isArray(jsonml)) { acc.push("<"); acc.push(jsonml[0]); var pos = 1; var attributes = jsonml[1]; if(attributes && !isArray(attributes) && typeof(attributes) !== "string") { for(var key in attributes) { if(attributes.hasOwnProperty(key)) { acc.push(' '); acc.push(key); acc.push('="'); xmlEscape(attributes[key], acc); acc.push('"'); } } ++pos; } if(pos < jsonml.length) { acc.push(">"); do { toXmlAcc(jsonml[pos], acc); ++pos; } while(pos < jsonml.length); acc.push("</"); acc.push(jsonml[0]); acc.push(">"); } else { acc.push(" />"); } } else { xmlEscape(jsonml.toString(), acc); } } // XML escaped entity table var entities = { "quot": '"', "amp": '&', "apos": "'", "lt": '<', "gt": '>' }; // Generate a reverse xml entity table. var reventities = (function () { var result = {}; for(var key in entities) { if(entities.hasOwnProperty(key)) { result[entities[key]] = key; } } return result; })(); // Append the characters of `str`, or the xml-entity they map to, to the `acc`umulator array. function xmlEscape(str, acc) { for(var i = 0; i < str.length; ++i) { var c = str[i]; var code = c.charCodeAt(0); var s = reventities[c]; if(s) { acc.push("&" + s + ";"); } else if(/*code < 32 ||*/ code >= 128) { acc.push("&#" + code + ";"); } else { acc.push(c); } } } // ## Utility functions // Apply a function to all the child elements of a given jsonml array. var childReduce = exports.childReduce = function(jsonml, fn, acc) { var first = jsonml[1]; if(typeof(first) !== "object" || isArray(first)) { acc = fn(acc, first); } for(var pos = 2; pos < jsonml.length; ++pos) { acc = fn(acc, jsonml[pos]); } return acc; }; // - `jsonml.ensureAttributeObject(jsonml_array)` changes an jsonml array such that it has a (possibly empty) attribute object at position 1 exports.ensureAttributeObject = function(jsonml) { if(typeof jsonml[1] !== "object" || jsonml[1].constructor === Array) { jsonml.unshift(jsonml[0]); jsonml[1] = {}; } }; exports.getAttr = function(jsonml, attribute) { if(typeof jsonml[1] !== "object" || jsonml[1].constructor === Array) { return undefined; } else { return jsonml[1][attribute]; } }; // Convert jsonml into an easier subscriptable json structure, not preserving // the order of the elements exports.toObject = function(jsonml) { var result = {}; result[jsonml[0]] = toObjectInner(jsonml); return result; }; // Internal function called by toObject. Return an object corresponding to // the child nodes of the `jsonml`-parameter function toObjectInner(jsonml) { var result = {}; var attr = jsonml[1]; var pos; if(typeof(attr) === "object" && !isArray(attr)) { for(var key in attr) { if(attr.hasOwnProperty(key)) { result["@" + key] = attr[key]; } } pos = 2; } else { pos = 1; if(jsonml.length === 2 && !isArray(attr)) { return attr; } } while(pos < jsonml.length) { var current = jsonml[pos]; if(isArray(current)) { addprop(result, current[0], toObjectInner(current)); } else { addprop(result, "_", current); } ++pos; } return result; } // Add a property to the object. If the property is already there, append // the `val`ue to an array at the key instead, possibly putting existing // object in front of such array, if that is not an array yet. function addprop(obj, key, val) { if(obj[key]) { if(isArray(obj[key])) { obj[key].push(val); } else { obj[key] = [obj[key], val]; } } else { obj[key] = val; } } // Error handler function JsonML_Error(desc) { throw desc; } // Array check, implemented here to avoid depending on any library function isArray(a) { return a !== null && typeof(a) === "object" && a.constructor === Array; }