hedera-web/js/vn/builder.js

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);
}
}
});