var VnObject = require ('./object'); var Component = require ('./component'); var Type = require ('./type'); var specialAttrs = { id : 1, property : 1 }; var objectAttrs = { for : 1 }; /** * Creates a object from a XML specification. */ module.exports = new Class ({ Extends: VnObject ,_addedMap: {} ,_contexts: null /** * Compiles an XML file. * * @param {String} path The XML path * @return {Boolean} %true on success, %false othersise */ ,compileFile: function (path) { this._path = path; return this.compileDocument (Vn.getXml (path)); } /** * Compiles an XML string. * * @param {String} xmlString The XML string * @return {Boolean} %true on success, %false othersise */ ,compileString: function (xmlString) { var parser = new DOMParser (); var doc = parser.parseFromString (xmlString, 'text/xml'); return this.compileDocument (doc); } /** * Compiles a XML document. * * @param {Document} doc The DOM document * @return {Boolean} %true on success, %false othersise */ ,compileDocument: function (doc) { if (!doc) return false; this._compileInit (); var docElement = doc.documentElement; if (docElement.tagName !== 'vn') { this._showError ('The toplevel tag should be named \'vn\''); 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. * * @param {Node} path The DOM node * @return {Boolean} %true on success, %false othersise */ ,compileNode: function (node) { this._compileInit (); this._mainContext = this._compileNode (node).id; this._compileEnd (); return true; } /** * Creates a new scope from a compiled XML tree. * * @param {Document} dstDocument The document used to create the nodes * @param {Object} signalData The object where to bind methods * @param {Scope} parentScope The parent scope or %null for no parent * @return {Scope} The created scope */ ,load: function (dstDocument, signalData, parentScope, extraObjects) { if (this._contexts === null) return null; this._doc = dstDocument || document; var contexts = this._contexts; var len = contexts.length; var objects = new Array (len); var scope = new Scope (this, objects, signalData, parentScope, extraObjects) for (var i = 0; i < len; i++) { var context = contexts[i]; if (context.tagName) objects[i] = this.elementInstantiate (context, scope); else if (context.klass) objects[i] = this.objectInstantiate (context, scope); else objects[i] = this.textInstantiate (context, scope); } return scope; } ,_compileInit: function () { this._path = null; this._tags = {}; this._contexts = []; this._contextMap = {}; this._objectLinks = []; this._mainContext = null; } ,_compileEnd: function () { var links = this._objectLinks; for (var i = links.length - 1; i >= 0; i--) { var link = links[i]; var context = link.context; var contextId = this._contextMap[link.objectId]; if (contextId == undefined) continue; if (link.prop) context.objectProps[link.prop] = contextId; else context.childs.push (contextId); links.splice (i, 1); } } ,_compileNode: function (node) { var context = null; var tagName = null; var isElement = node.nodeType === Node.ELEMENT_NODE; if (isElement) 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 (isElement) { var nodeId = node.getAttribute ('id'); if (nodeId) { this._contextMap[nodeId] = context.id; context.nodeId = nodeId; } var tags = this._tags[tagName]; if (!tags) this._tags[tagName] = tags = []; tags.push (context.id); } this._contexts.push (context); return context; } ,link: function (scope) { var objects = scope.objects; var links = this._objectLinks; for (var i = links.length - 1; i >= 0; i--) { var link = links[i]; var object = objects[link.context.id]; var objectRef = scope.$(link.objectId); if (objectRef == null) { this._showError ('Referenced unexistent object with id \'%s\'', link.objectId); continue; } if (link.prop) object[link.prop] = objectRef; else object.appendChild (objectRef); } 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, scope); else if (context.klass) this.objectLink (context, object, objects, scope); } for (var i = 0; i < contexts.length; i++) { var context = contexts[i]; var object = objects[i]; if (context.tagName) this.elementConnect (context, object, objects, scope); else if (context.klass) this.objectConnect (context, object, objects, scope); } } /** * Creates a object context. */ ,objectCompile: function (node, tagName) { var klass = vnCustomTags[tagName]; if (!klass) return null; var props = {}; var objectProps = {}; var funcProps = {}; var childs = []; var events = {}; var context = { klass: klass, props: props, objectProps: objectProps, funcProps: funcProps, 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)) events[attribute.substr (3)] = value; else if (!specialAttrs[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.objectAddLink (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; } var propError = false; 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: context.funcProps[propName] = value; break; case Type: newValue = window[value]; break; default: if (propInfo.enumType) newValue = propInfo.enumType[value]; else if (propInfo.type instanceof Function) isLink = true; else propError = true; } if (isLink) this.objectAddLink (context, propName, value); else if (newValue != null) props[propName] = newValue; else if (propError) this._showError ('Attribute \'%s\' invalid for tag \'%s\'', attribute, node.tagName); } ,objectAddLink: function (context, prop, objectId) { this._objectLinks.push ({ context: context ,prop: prop ,objectId: objectId }); } ,objectInstantiate: function (context, scope) { var object = new context.klass (); object.setProperties (context.props); if (context.nodeId && object instanceof Component) object.htmlId = scope.getHtmlId (context.nodeId); return object; } ,objectLink: function (context, object, objects, scope) { 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]]); if (context.custom) object.loadXml (scope, context.custom); } ,objectConnect: function (context, object, objects, scope) { var funcProps = context.funcProps; for (var prop in funcProps) { var method = scope.getMethod (funcProps[prop], true); if (method) object[prop] = method; } var events = context.events; for (var event in events) { var method = scope.getMethod (events[event]); if (method) object.on (event, method, scope.signalData); } } /** * Creates a HTML node context. */ ,elementCompile: function (node, tagName) { var props = {}; var objectProps = {}; var childs = []; var events = {}; 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)) events[attribute.substr (3)] = value; else if (objectAttrs[attribute]) objectProps[attribute] = value; else if (!specialAttrs[attribute]) props[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, props: props, objectProps: objectProps, childs: childs, events: events }; } ,elementInstantiate: function (context, scope) { var object = this._doc.createElement (context.tagName); var props = context.props; for (var prop in props) object.setAttribute (prop, props[prop]); if (context.nodeId) object.setAttribute ('id', scope.getHtmlId (context.nodeId)); return object; } ,elementLink: function (context, object, objects, scope) { var props = context.objectProps; for (var prop in props) { var objectValue = scope.$(props[prop]); var htmlId; if (objectValue instanceof Component) htmlId = objectValue.htmlId; if (objectValue instanceof Node) htmlId = objectValue.id; object.setAttribute (prop, htmlId); } var childs = context.childs; for (var i = 0; i < childs.length; i++) { var child = objects[childs[i]]; if (child instanceof Component) child = child.node; if (child instanceof Node) object.appendChild (child); } } ,elementConnect: function (context, object, objects, scope) { var events = context.events; for (var event in events) { var method = scope.getMethod (events[event], true); if (method) object.addEventListener (event, method); } } /** * Creates a text node context. */ ,textCompile: function (node, tagName) { if (!tagName) { var text = node.textContent; if (/^[\s]*\\?_.*/.test (text)) text = _(text.trim ().substr (1)); } else if (tagName === 't') var text = _(node.firstChild.textContent); else return null; return {text: text}; } ,textInstantiate: function (context) { return this._doc.createTextNode (context.text); } ,_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); } ,_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; } ,_isEvent: function (attribute) { return /^on-\w+/.test (attribute); } ,_replaceFunc: function (token) { return token.charAt(1).toUpperCase (); } ,getMain: function (result) { return result.objects[this._mainContext]; } ,getById: function (objectId) { return this._contextMap[objectId]; } ,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 []; } }); var scopeUid = 0; var Scope = new Class ({ Extends: VnObject ,initialize: function (builder, objects, signalData, parentScope, extraObjects) { this.builder = builder; this.objects = objects; this.signalData = signalData; this.parentScope = parentScope; this.uid = ++scopeUid; this.extraObjects = extraObjects; if (!signalData && parentScope) this.signalData = parentScope.signalData; this.parent (); } ,getMain: function () { return this.builder.getMain (this); } /** * Fetchs an element by it's identifier. * * @param {String} id The node identifier */ ,$: function (id) { var object; var index = this.builder.getById (id); if (index !== undefined) object = this.objects[index]; else { object = this.extraObjects[id]; if (object === undefined && this.parentScope) object = this.parentScope.getById (id); } return object ? object : null; } /** * Fetchs an element by it's identifier. * * @param {String} id The node identifier */ ,getById: function (id) { return this.$(id); } ,getByTagName: function (tagName) { return this.builder.getByTagName (this, tagName); } ,link: function () { this.builder.link (this); } ,getMethod: function (value, binded) { if (this.signalData) { var method = this.signalData[value]; if (method && binded) method = method.bind (this.signalData); } else var method = window[value]; if (method === undefined) this.builder._showError ('Function \'%s\' not found', value); return method; } ,getHtmlId: function (nodeId) { return 'vn-'+ this.uid +'-'+ nodeId; } ,_destroy: function () { var objects = this.objects; for (var i = objects.length; i--;) if (objects[i] instanceof VnObject) { objects[i].unref (); objects[i].disconnectByInstance (this.builder.signalData); } this.parent (); } });