const VnObject = require('./object'); const Widget = require('../htk/widget'); const VnNode = require('./node'); const Scope = require('./scope'); const kebabToCamel = require('./string-util').kebabToCamel; /** * Creates a object from a XML specification. */ module.exports = new Class({ Extends: VnObject ,_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, exprArgs) { if (!doc) return false; this._preCompile(exprArgs); 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._compile(childs[i]); this._postCompile(); return true; } /** * Compiles a single DOM node. * * @path Node The DOM node * @return %true on success, %false othersise */ ,compileNode: function(node, exprArgs) { this._preCompile(exprArgs); this._mainContext = this._compile(node).id; this._postCompile(); return true; } /** * Called before starting to compile nodes. */ ,_preCompile: function(exprArgs) { this._path = null; this._tags = {}; this._contexts = []; this._contextMap = {}; this._links = []; this._mainContext = null; this._baseExprArgs = ['_', '$']; if (exprArgs) this._baseExprArgs = this._baseExprArgs.concat(exprArgs); this._baseEventArgs = this._baseExprArgs.concat(['$event']); this._exprArgs = this._baseExprArgs.join(','); this._eventArgs = this._baseEventArgs.join(','); } /** * Called after all nodes have been compiled. */ ,_postCompile: function() {} /** * Compiles a node. */ ,_compile: function(node) { let context = null; let tagName = null; const 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; 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[kebabToCamel(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; } ,getMain: function(scope) { return scope.objects[this._mainContext]; } ,getByTagName: function(scope, tagName) { var tags = this._tags[tagName]; if (tags) { var arr = new Array(tags.length); for (var i = 0; i < tags.length; i++) arr[i] = scope.objects[tags[i]]; return arr; } return []; } ,load: function(dstDocument, thisArg, parentScope) { if (this._contexts === null) return null; const doc = dstDocument ? dstDocument : document; const contexts = this._contexts; const len = contexts.length; const objects = new Array(len); const scope = new Scope(this, objects, thisArg, parentScope); for (var i = 0; i < len; i++) { var context = contexts[i]; if (context.tagName) objects[i] = this.elementInstantiate(doc, context, scope); else if (context.klass) objects[i] = this.objectInstantiate(doc, context, scope); else objects[i] = this.textInstantiate(doc, context, scope); } return scope; } ,link: function(scope, exprScope) { const objects = scope.objects; const links = this._links; // Pre-link for (var i = links.length - 1; i >= 0; i--) { const link = links[i]; const object = objects[link.context.id]; const objectRef = scope.$[link.objectId]; if (objectRef === undefined) { this.showError('Referenced unexistent object with id \'%s\'', link.objectId); continue; } if (link.prop) object[link.prop] = objectRef; else object.appendChild(objectRef); } // Post-link const baseExprScope = [ _, scope.$ ].concat(exprScope); this.linkExpr(scope, baseExprScope); const contexts = this._contexts; for (var i = 0; i < contexts.length; i++) { const context = contexts[i]; const object = objects[i]; if (context.tagName) this.elementLink(context, object, objects, scope, baseExprScope); else if (context.klass) this.objectLink(context, object, objects, scope, baseExprScope); } } ,linkExpr(scope, baseScope, exprScope) { const contexts = this._contexts; const objects = scope.objects; exprScope = baseScope.concat(exprScope); 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(scope.thisArg, exprScope); } 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(scope.thisArg, exprScope); } catch (e) { console.warn('Expression error:', e.message); continue; } if (context.tagName) object.setAttribute(prop, value); else object[prop] = value; } } } } ,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 ,prop ,objectId: kebabToCamel(objectId) }); } ,fnExpr(expr) { return new Function(this._exprArgs, '"use strict"; return ' + expr + ';' ); } ,matchExpr(value) { const match = /^{{(.*)}}$/.exec(value); if (!match) return null; return this.fnExpr(match[1]); } ,_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) { let method; if (this.isIdentifier(value)) { // XXX: Compatibility with old events method = value; } else { try { method = new Function(this._eventArgs, '"use strict"; return ' + value + ';' ); } catch (err) { this.showError(`Method: ${err.message}: ${value}`); } } return method; } ,_isEvent: function(attribute) { return /^on-\w+/.test(attribute); } ,isIdentifier: function(value) { return /^[a-zA-Z_$][\w$]*$/.test(value); } ,_replaceFunc: function(token) { return token.charAt(1).toUpperCase(); } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TextNode /** * 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(doc, context) { return doc.createTextNode(context.exprs ? '' : context.text); } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Vn.Object /** * 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, dynProps: {}, funcProps: {}, 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 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._compile(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, node, attribute, value) { let isLink = false; let propError = false; let newValue = null; const propName = attribute.replace(/-./g, this._replaceFunc); const 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) { context.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: context.funcProps[propName] = this._getMethod(value); break; default: if (propInfo.enumType) newValue = propInfo.enumType[value]; else if (propInfo.type instanceof Function) isLink = true; else propError = true; } if (isLink) this._addLink(context, propName, value); else if (newValue !== null && newValue !== undefined) props[propName] = newValue; else if (propError) this.showError('Attribute \'%s\' invalid for tag \'%s\'', attribute, node.tagName); } } ,objectInstantiate: function(doc, context, scope) { const object = new context.klass(); object.setProperties(context.props); if (context.nodeId && object instanceof Widget) { var id = context.nodeId; object.htmlId = scope.getHtmlId(id); object.className = '_'+ id +' '+ object.className; } return object; } ,objectLink: function(context, object, objects, scope, exprScope) { const objectProps = context.objectProps; for (const prop in objectProps) object[prop] = objects[objectProps[prop]]; const childs = context.childs; for (let i = 0; i < childs.length; i++) object.appendChild(objects[childs[i]]); const funcProps = context.funcProps; for (const prop in funcProps) { let method; const handler = funcProps[prop]; if (typeof handler === 'string') { // XXX: Compatibility with old expressions method = scope.thisArg[handler]; if (!method) this.showError(`Function '${handler}' not found`); method = method.bind(scope.thisArg); } else { method = function() { handler.apply(scope.thisArg, exprScope); }; } if (method) object[prop] = method; } const events = context.events; for (const event in events) { let listener; const handler = events[event]; if (typeof handler === 'string') { // XXX: Compatibility with old expressions listener = scope.thisArg[handler]; if (!listener) this.showError(`Function '${handler}' not found`); } else { listener = function() { handler.apply(scope.thisArg, exprScope.concat(arguments)); }; } if (listener) object.on(event, listener, scope.thisArg); } if (context.custom) object.loadXml(scope, context.custom); } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Element /** * 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; if (this._isEvent(attribute)) { var handler = this._getMethod(value); if (handler) events[attribute.substr(3)] = handler; } else if (attribute !== 'id') { const expr = this.matchExpr(value); if (expr) dynProps[attribute] = expr; else attributes[attribute] = this._translateValue(value); } } var childContext; var childNodes = node.childNodes; if (childNodes) for (var i = 0; i < childNodes.length; i++) if (childContext = this._compile(childNodes[i])) childs.push(childContext.id); return { tagName, attributes, dynProps, childs, events }; } ,elementInstantiate: function(doc, context, scope) { var object = doc.createElement(context.tagName); const attributes = context.attributes; for (const attribute in attributes) object.setAttribute(attribute, attributes[attribute]); if (context.nodeId) { const id = context.nodeId; object.setAttribute('id', scope.getHtmlId(id)); VnNode.addClass(object, '_'+ id); } return object; } ,elementLink: function(context, object, objects, scope, exprScope) { const childs = context.childs; for (var i = 0; i < childs.length; i++) { let child = objects[childs[i]]; if (child instanceof Widget) child = child.node; if (child instanceof Node) object.appendChild(child); } const events = context.events; for (const event in events) { let listener; const handler = events[event]; if (typeof handler === 'string') { // XXX: Compatibility with old expressions listener = scope.thisArg[handler]; if (!listener) this.showError(`Function '${handler}' not found`); listener = listener.bind(scope.thisArg); } else { listener = function(e) { handler.apply(scope.thisArg, exprScope.concat(e)); }; } if (listener) object.addEventListener(event, listener); } } });