var VnObject = require ('./object'); var Type = require ('./type'); /** * Creates a object from a XML specification. */ module.exports = new Class ({ Extends: VnObject ,_addedMap: {} ,_contexts: null ,add: function (id, object) { this._addedMap[id] = object; } ,setParent: function (parentResult) { this._parentResult = parentResult; if (parentResult && !this.signalData) this.signalData = parentResult.builder.signalData; } ,getMain: function (result) { return result.objects[this._mainContext]; } ,getById: function (result, objectId) { var index = this._contextMap[objectId]; if (index !== undefined) return result.objects[index]; var object = this._addedMap[objectId]; if (object !== undefined) return object; if (this._parentResult) return this._parentResult.getById (objectId); return null; } ,getByTagName: function (result, tagName) { var tags = this._tags[tagName]; if (tags) { var arr = new Array (tags.length); for (var i = 0; i < tags.length; i++) arr[i] = result.objects[tags[i]]; return arr; } return []; } /** * Compiles an XML file. * * @path String The XML path * @dstDocument Document The document used to create the nodes * @return %true on success, %false othersise */ ,loadXml: function (path, dstDocument) { this._path = path; return this.loadFromXmlDoc (Vn.getXml (path), dstDocument); } ,loadFromString: function (xmlString, dstDocument) { var parser = new DOMParser (); var xmlDoc = parser.parseFromString (xmlString, 'text/xml'); return this.loadFromXmlDoc (xmlDoc, dstDocument); } ,loadFromXmlDoc: function (xmlDoc, dstDocument) { if (!xmlDoc) return false; this._compileInit (dstDocument); var docElement = xmlDoc.documentElement; if (docElement.tagName !== 'vn') { this._showError ('Malformed XML'); this._contexts = null; return false; } var childs = docElement.childNodes; if (childs) for (var i = 0; i < childs.length; i++) this._compileNode (childs[i]); this._compileEnd (); return true; } /** * Compiles a single DOM node. * * @path Node The DOM node * @dstDocument Document The document used to create the nodes * @return %true on success, %false othersise */ ,loadXmlFromNode: function (node, dstDocument) { this._compileInit (dstDocument); this._mainContext = this._compileNode (node).id; this._compileEnd (); return true; } ,load: function () { if (this._contexts === null) return null; var contexts = this._contexts; var len = contexts.length; var objects = new Array (len); for (var i = 0; i < len; i++) { var context = contexts[i]; if (context.tagName) objects[i] = this.elementInstantiate (context); else if (context.klass) objects[i] = this.objectInstantiate (context); else objects[i] = this.textInstantiate (context); } return new BuilderResult (this, objects); } ,link: function (result) { var objects = result.objects; for (var i = this._links.length - 1; i >= 0; i--) { var l = this._links[i]; var addedObject = this._addedMap[l.objectId]; if (addedObject) { if (l.prop) objects[l.context.id][l.prop] = addedObject; else objects[l.context.id].appendChild (addedObject); } else this._showError ('Referenced unexistent object with id \'%s\'', l.objectId); } var contexts = this._contexts; for (var i = 0; i < contexts.length; i++) { var context = contexts[i]; var object = objects[i]; if (context.tagName) this.elementLink (context, object, objects, result); else if (context.klass) this.objectLink (context, object, objects, result); } } ,_compileInit: function (dstDocument) { this._path = null; this._tags = {}; this._contexts = []; this._contextMap = {}; this._links = []; this._mainContext = null; this._doc = dstDocument ? dstDocument : document; } ,_compileEnd: function () { for (var i = this._links.length - 1; i >= 0; i--) { var l = this._links[i]; var contextId = this._contextMap[l.objectId]; if (contextId != undefined) { if (l.prop) l.context.objectProps[l.prop] = contextId; else l.context.childs.push (contextId); this._links.splice (i, 1); } else { var object = this._addedMap[l.objectId]; if (!object && this._parentResult) object = this._parentResult.getById (l.objectId); if (object) { l.context.props[l.prop] = object; this._links.splice (i, 1); } } } } ,_compileNode: function (node) { var context = null; var tagName = null; if (node.nodeType === Node.ELEMENT_NODE) tagName = node.tagName.toLowerCase (); else if (node.nodeType !== Node.TEXT_NODE || /^[\n\r\t]*$/.test (node.textContent)) return null; var context = this.textCompile (node, tagName) || this.objectCompile (node, tagName) || this.elementCompile (node, tagName); context.id = this._contexts.length; if (tagName) { var nodeId = node.getAttribute ('id'); if (nodeId) this._contextMap[nodeId] = context.id; var tags = this._tags[tagName]; if (!tags) this._tags[tagName] = tags = []; tags.push (context.id); } this._contexts.push (context); return context; } /** * Creates a text node context. */ ,textCompile: function (node, tagName) { if (!tagName) var text = node.textContent; else if (tagName === 't') var text = _(node.firstChild.textContent); else return null; return {text: text}; } ,textInstantiate: function (context) { return this._doc.createTextNode (context.text); } /** * Creates a object context. */ ,objectCompile: function (node, tagName) { var klass = vnCustomTags[tagName]; if (!klass) return null; var props = {}; var objectProps = {}; var childs = []; var events = {}; var context = { klass: klass, props: props, objectProps: objectProps, childs: childs, events: events, custom: null }; var a = node.attributes; for (var i = 0; i < a.length; i++) { var attribute = a[i].nodeName; var value = a[i].nodeValue; if (this._isEvent (attribute)) { var handler = this._getMethod (value) if (handler) events[attribute.substr (3)] = handler; } else if (!/^(id|property)$/.test (attribute)) { this.propCompile (context, klass, props, node, attribute, value); } } var childNodes = node.childNodes; if (childNodes) for (var i = 0; i < childNodes.length; i++) { var child = childNodes[i]; var childContext = null; var childTagName = null; if (child.nodeType === Node.ELEMENT_NODE) childTagName = child.tagName.toLowerCase (); if (childTagName === 'pointer') { this._addLink (context, null, child.getAttribute ('object')); } else if (childTagName === 'custom') { context.custom = child; } else if (childContext = this._compileNode (child)) { var prop = null; if (childTagName) prop = child.getAttribute ('property'); if (prop) { prop = prop.replace (/-./g, this._replaceFunc); objectProps[prop] = childContext.id; } else childs.push (childContext.id); } } return context; } ,propCompile: function (context, klass, props, node, attribute, value) { var isLink = false; var newValue = null; var propName = attribute.replace (/-./g, this._replaceFunc); var propInfo = klass.Properties[propName]; if (!propInfo) { this._showError ('Attribute \'%s\' not valid for tag \'%s\'', attribute, node.tagName); return; } if (!value) { this._showError ('Attribute \'%s\' empty on tag \'%s\'', attribute, node.tagName); return; } switch (propInfo.type) { case null: newValue = value; break; case Boolean: newValue = (/^(true|1)$/i).test (value); break; case Number: newValue = 0 + new Number (value); break; case String: newValue = this._translateValue (value); break; case Function: var method = this._getMethod (value); newValue = method ? method.bind (this.signalData) : null; break; case Type: newValue = window[value]; break; default: if (propInfo.enumType) newValue = propInfo.enumType[value]; else if (propInfo.type instanceof Function) isLink = true; } if (isLink) this._addLink (context, propName, value); else if (newValue !== null && newValue !== undefined) props[propName] = newValue; else this._showError ('Attribute \'%s\' invalid for tag \'%s\'', attribute, node.tagName); } ,objectInstantiate: function (context) { return new context.klass (); } ,objectLink: function (context, object, objects, res) { object.setProperties (context.props); var objectProps = context.objectProps; for (var prop in objectProps) object[prop] = objects[objectProps[prop]]; var childs = context.childs; for (var i = 0; i < childs.length; i++) object.appendChild (objects[childs[i]]); var events = context.events; for (var event in events) object.on (event, events[event], this.signalData); if (context.custom) object.loadXml (res, context.custom); } /** * Creates a HTML node context. */ ,elementCompile: function (node, tagName) { var attributes = {}; var childs = []; var events = {}; var handler; var a = node.attributes; for (var i = 0; i < a.length; i++) { var attribute = a[i].nodeName; var value = a[i].nodeValue; if (this._isEvent (attribute)) { var handler = this._getMethod (value); if (handler) events[attribute.substr (3)] = handler; } else if (attribute !== 'id') attributes[attribute] = this._translateValue (value); } var childContext; var childNodes = node.childNodes; if (childNodes) for (var i = 0; i < childNodes.length; i++) if (childContext = this._compileNode (childNodes[i])) childs.push (childContext.id); return { tagName: tagName, attributes: attributes, childs: childs, events: events }; } ,elementInstantiate: function (context) { return this._doc.createElement (context.tagName); } ,elementLink: function (context, object, objects) { var attributes = context.attributes; for (var attribute in attributes) object.setAttribute (attribute, attributes[attribute]); var childs = context.childs; for (var i = 0; i < childs.length; i++) { var child = objects[childs[i]]; if (child instanceof Htk.Widget) child = child.node; if (child instanceof Node) object.appendChild (child); } var events = context.events; for (var event in events) object.addEventListener (event, events[event].bind (this.signalData)); } ,_showError: function (error) { var path = this._path ? this._path : 'Node'; var logArgs = ['Vn.Builder: %s: '+ error, path]; for (var i = 1; i < arguments.length; i++) logArgs.push (arguments[i]); console.warn.apply (console, logArgs); } ,_addLink: function (context, prop, objectId) { this._links.push ({ context: context ,prop: prop ,objectId: objectId }); } ,_translateValue: function (value) { var chr = value.charAt (0); if (chr === '_') return _(value.substr (1)); else if (chr === '\\' && value.charAt (1) === '_') return value.substr (1); return value; } ,_getMethod: function (value) { if (this.signalData) var method = this.signalData[value]; else var method = window[value]; if (method === undefined) this._showError ('Function \'%s\' not found', value); return method; } ,_isEvent: function (attribute) { return /^on-\w+/.test (attribute); } ,_replaceFunc: function (token) { return token.charAt(1).toUpperCase (); } }); var BuilderResult = new Class ({ Extends: VnObject ,initialize: function (builder, objects) { this.builder = builder; this.objects = objects; } ,getMain: function () { return this.builder.getMain (this); } ,$: function (objectId) { return this.builder.getById (this, objectId); } ,getById: function (objectId) { return this.builder.getById (this, objectId); } ,getByTagName: function (tagName) { return this.builder.getByTagName (this, tagName); } ,link: function () { this.builder.link (this); } ,_destroy: function () { var objects = this.objects; for (var i = objects.length; i--;) if (objects[i] instanceof VnObject) objects[i].unref (); this.parent (); } });