forked from verdnatura/hedera-web
676 lines
15 KiB
JavaScript
676 lines
15 KiB
JavaScript
const VnObject = require('./object');
|
|
const Component = require('./component');
|
|
const VnNode = require('./node');
|
|
const Scope = require('./scope');
|
|
const Type = require('./type');
|
|
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;
|
|
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._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 Component) {
|
|
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 Component)
|
|
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);
|
|
}
|
|
}
|
|
});
|