var Object = require('./object'); /** * Creates a object from a XML specification. */ module.exports = new Class({ Extends: Object ,_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, scope) { if (!xmlDoc) return false; this._compileInit(dstDocument, scope); 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, scope) { this._compileInit(dstDocument, scope); 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, self, scope) { 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); } this.linkExpr(result, self, scope); 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); } } ,fnExpr(expr) { return new Function(this._scopeArgs, '"use strict"; return ' + expr + ';' ); } ,matchExpr(value) { const match = /^{{(.*)}}$/.exec(value); if (!match) return null; return this.fnExpr(match[1]); } ,linkExpr(result, self, scope) { const contexts = this._contexts; const objects = result.objects; let args = [_] if (scope) args = args.concat(scope); for (let i = 0; i < contexts.length; i++) { const context = contexts[i]; const object = objects[i]; if (context.exprs) { const values = []; for (expr of context.exprs) { let value = undefined; try { value = expr.apply(self, args); } catch (e) { console.warn('Expression error:', e.message); continue; } values.push(value); } let k = 0; const text = context.text.replace(/{{\d+}}/g, function() { return values[k++]; }); object.textContent = text; } else { const dynProps = context.dynProps; for (const prop in dynProps) { let value = undefined; try { value = dynProps[prop].apply(self, args); } catch (e) { console.warn('Expression error:', e.message); continue; } if (context.tagName) object.setAttribute(prop, value); else object[prop] = value; } } } } ,_compileInit: function(dstDocument, scope) { this._path = null; this._tags = {}; this._contexts = []; this._contextMap = {}; this._links = []; this._mainContext = null; this._doc = dstDocument ? dstDocument : document; this._scope = ['_']; if (scope) this._scope = this._scope.concat(scope); this._scopeArgs = this._scope.join(','); } ,_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) { let text = node.textContent; if (/{{.*}}/.test(text)) { let i = 0; const self = this; const exprs = []; text = text.replace(/{{((?:(?!}}).)*)}}/g, function(match, capture) { exprs.push(self.fnExpr(capture)); return `{{${i++}}}`; }); return {text, exprs}; } else return {text}; } else if (tagName === 't') return {text: _(node.firstChild.textContent)}; else return null; } ,textInstantiate: function(context) { return this._doc.createTextNode(context.exprs ? '' : context.text); } /** * Creates a object context. */ ,objectCompile: function(node, tagName) { var klass = vnCustomTags[tagName]; if (!klass) return null; var props = {}; var dynProps = {}; var objectProps = {}; var childs = []; var events = {}; var context = { klass: klass, props: props, dynProps: dynProps, 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, dynProps, node, attribute, value); } } var childNodes = node.childNodes; if (childNodes) for (var i = 0; i < childNodes.length; i++) { var child = childNodes[i]; var isElement = child.nodeType === Node.ELEMENT_NODE; var childTagName = isElement ? child.tagName.toLowerCase() : null; var childContext; 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 = isElement ? child.getAttribute('property') : null; if (prop) { prop = prop.replace(/-./g, this._replaceFunc); objectProps[prop] = childContext.id; } else childs.push(childContext.id); } } return context; } ,propCompile: function(context, klass, props, dynProps, 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; } const expr = this.matchExpr(value); if (expr) { dynProps[propName] = expr; } else { switch (propInfo.type) { 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; 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 dynProps = {}; 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; const expr = this.matchExpr(value); if (expr) { dynProps[attribute] = expr; } else 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, attributes, dynProps, childs, 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(null, 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: Object ,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(self, scope) { this.builder.link(this, self, scope); } ,_destroy: function() { var objects = this.objects; for (var i = 0; i < objects.length; i++) if (objects[i] instanceof Object) objects[i].unref(); this.parent(); } });