![JAR search and dependency download from the Maven repository](/logo.png)
features.opensocial-templates.compiler.js Maven / Gradle / Ivy
Go to download
Packages all the features that shindig provides into a single jar file to allow
loading from the classpath
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* @fileoverview Implements compiler functionality for OpenSocial Templates.
*
* TODO(davidbyttow): Move into os.Compiler.
*/
/**
* Literal semcolons have special meaning in JST, so we need to change them to
* variable references.
*/
os.SEMICOLON = ';';
/**
* Check if the browser is Internet Explorer.
*
* TODO(levik): Find a better, more general way to do this, esp. if we need
* to do other browser checks elswhere.
*/
os.isIe = navigator.userAgent.indexOf('Opera') != 0 &&
navigator.userAgent.indexOf('MSIE') != -1;
/**
* Takes an XML node containing Template markup and compiles it into a Template.
* The node itself is not considered part of the markup.
* @param {Node} node XML node to be compiled.
* @param {string=} opt_id An optional ID for the new template.
* @return {os.Template} A compiled Template object.
*/
os.compileXMLNode = function(node, opt_id) {
var nodes = [];
for (var child = node.firstChild; child; child = child.nextSibling) {
if (child.nodeType == DOM_ELEMENT_NODE) {
nodes.push(os.compileNode_(child));
} else if (child.nodeType == DOM_TEXT_NODE) {
if (child != node.firstChild ||
!child.nodeValue.match(os.regExps_.ONLY_WHITESPACE)) {
var compiled = os.breakTextNode_(child);
for (var i = 0; i < compiled.length; i++) {
nodes.push(compiled[i]);
}
}
}
}
var template = new os.Template(opt_id);
template.setCompiledNodes_(nodes);
return template;
};
/**
* Takes an XML Document and compiles it into a Template object.
* @param {Document} doc XML document to be compiled.
* @param {string=} opt_id An optional ID for the new template.
* @return {os.Template} A compiled Template object.
*/
os.compileXMLDoc = function(doc, opt_id) {
var node = doc.firstChild;
// Find the node (skip DOCTYPE).
while (node.nodeType != DOM_ELEMENT_NODE) {
node = node.nextSibling;
}
return os.compileXMLNode(node, opt_id);
};
/**
* Map of special operators to be transformed.
*/
os.operatorMap = {
'and': '&&',
'eq': '==',
'lte': '<=',
'lt': '<',
'gte': '>=',
'gt': '>',
'neq': '!=',
'or': '||',
'not': '!'
};
/**
* Shared regular expression to split a string into lexical parts. Quoted
* strings are treated as tokens, so are identifiers and any characters between
* them.
* In "foo + bar = 'baz - bing'", the tokens are
* ["foo", " + ", "bar", " = ", "'baz - bing'"]
*/
os.regExps_.SPLIT_INTO_TOKENS =
/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\w+|[^"'\w]+/g;
/**
* Parses operator markup into JS code. See operator map above.
*
* TODO: Simplify this to only work on neccessary operators - binary ones that
* use "<" or ">".
*
* @param {string} src The string snippet to parse.
* @private
*/
os.remapOperators_ = function(src) {
return src.replace(os.regExps_.SPLIT_INTO_TOKENS,
function(token) {
return os.operatorMap.hasOwnProperty(token) ?
os.operatorMap[token] : token;
});
};
/**
* Remap variable references in the expression.
* @param {string} expr The expression to transform.
* @return {string} Transformed exression.
* @private
*/
os.transformVariables_ = function(expr) {
expr = os.replaceTopLevelVars_(expr);
return expr;
};
/**
* Map of variables to transform
* @private
*/
os.variableMap_ = {
'my': os.VAR_my,
'My': os.VAR_my,
'cur': VAR_this,
'Cur': VAR_this,
'$cur': VAR_this,
'Top': VAR_top,
'Context': VAR_loop
};
/**
* Replace the top level variables
* @param {string} text The expression.
* @return {string} Expression with replacements.
* @private
*/
os.replaceTopLevelVars_ = function(text) {
var regex;
regex = os.regExps_.TOP_LEVEL_VAR_REPLACEMENT;
if (!regex) {
regex = /(^|[^.$a-zA-Z0-9])([$a-zA-Z0-9]+)/g;
os.regExps_.TOP_LEVEL_VAR_REPLACEMENT = regex;
}
return text.replace(regex,
function(whole, left, right) {
if (os.variableMap_.hasOwnProperty(right)) {
return left + os.variableMap_[right];
} else {
return whole;
}
});
};
/**
* This function is used to lookup named properties of objects.
* By default only a simple lookup is performed, but using
* os.setIdentifierResolver() it's possible to plug in a more complex function,
* for example one that looks up foo -> getFoo() -> get("foo").
*
* TODO: This should not be in compiler.
* @private
*/
os.identifierResolver_ = function(data, name) {
return data.hasOwnProperty(name) ? data[name] : ('get' in data ? data.get(name) : null);
};
/**
* Sets the Identifier resolver function. This is global, and must be done
* before any compilation of templates takes place.
*
* TODO: This should possibly not be in compiler?
*/
os.setIdentifierResolver = function(resolver) {
os.identifierResolver_ = resolver;
};
/**
* Gets a named property from a JsEvalContext (by checking data_ and vars_) or
* from a simple JSON object by looking at properties. The IdentifierResolver
* function is used in either case.
*
* TODO: This should not be in compiler.
*
* @param {JsEvalContext|Object} context Context to get property from.
* @param {string} name Name of the property.
* @return {Object|string}
*/
os.getFromContext = function(context, name, opt_default) {
if (!context) {
return opt_default;
}
var ret;
// Check if this is a context object.
if (context.vars_ && context.data_) {
// Is the context payload a DOM node?
if (context.data_.nodeType == DOM_ELEMENT_NODE) {
ret = os.getValueFromNode_(context.data_, name);
if (ret == null) {
// Set to undefined
ret = void(0);
}
} else {
ret = os.identifierResolver_(context.data_, name);
}
if (typeof(ret) == 'undefined') {
ret = os.identifierResolver_(context.vars_, name);
}
if (typeof(ret) == 'undefined' && context.vars_[os.VAR_my]) {
ret = os.getValueFromNode_(context.vars_[os.VAR_my], name);
}
if (typeof(ret) == 'undefined' && context.vars_[VAR_top]) {
ret = context.vars_[VAR_top][name];
}
} else if (context.nodeType == DOM_ELEMENT_NODE) {
// Is the context a DOM node?
ret = os.getValueFromNode_(context, name);
} else {
ret = os.identifierResolver_(context, name);
}
if (typeof(ret) == 'undefined' || ret == null) {
if (typeof(opt_default) != 'undefined') {
ret = opt_default;
} else {
ret = '';
}
} else if (opt_default && os.isArray(opt_default) && !os.isArray(ret) &&
ret.list && os.isArray(ret.list)) {
// If we were trying to get an array, but got a JSON object with an
// array property "list", return that instead.
ret = ret.list;
}
return ret;
};
/**
* Prepares an expression for JS evaluation.
* @param {string} expr The expression snippet to parse.
* @param {string=} opt_default An optional default value reference (such as the
* literal string 'null').
* @private
*/
os.transformExpression_ = function(expr, opt_default) {
expr = os.remapOperators_(expr);
expr = os.transformVariables_(expr);
if (os.identifierResolver_) {
expr = os.wrapIdentifiersInExpression(expr, opt_default);
}
return expr;
};
/**
* A Map of special attribute names to change while copying attributes during
* compilation. The key is OST-spec attribute, while the value is JST attribute
* used to implement that feature.
* @private
*/
os.attributeMap_ = {
'if': ATT_display,
'repeat': ATT_select,
'cur': ATT_innerselect
};
/**
* Appends a JSTemplate attribute value while maintaining previous values.
* @private
*/
os.appendJSTAttribute_ = function(node, attrName, value) {
var previousValue = node.getAttribute(attrName);
if (previousValue) {
value = previousValue + ';' + value;
}
node.setAttribute(attrName, value);
};
/**
* Copies attributes from one node (xml or html) to another (html),.
* Special OpenSocial attributes are substituted for their JStemplate
* counterparts.
* @param {Element} from An XML or HTML node to copy attributes from.
* @param {Element} to An HTML node to copy attributes to.
* @param {string=} opt_customTag The name of the custom tag, being processed if
* any.
*
* TODO(levik): On IE, some properties/attributes might be case sensitive when
* set through script (such as "colSpan") - since they're not case sensitive
* when defined in HTML, we need to support this type of use.
* @private
*/
os.copyAttributes_ = function(from, to, opt_customTag) {
var dynamicAttributes = null;
for (var i = 0; i < from.attributes.length; i++) {
var name = from.attributes[i].nodeName;
var value = from.getAttribute(name);
if (name && value) {
if (name == 'var') {
os.appendJSTAttribute_(to, ATT_vars, from.getAttribute(name) +
': $this');
} else if (name == 'context') {
os.appendJSTAttribute_(to, ATT_vars, from.getAttribute(name) +
': ' + VAR_loop);
} else if (name.length < 7 || name.substring(0, 6) != 'xmlns:') {
if (os.customAttributes_[name]) {
os.appendJSTAttribute_(to, ATT_eval, "os.doAttribute(this, '" + name +
"', $this, $context)");
} else if (name == 'repeat') {
os.appendJSTAttribute_(to, ATT_eval,
'os.setContextNode_($this, $context)');
}
var outName = os.attributeMap_.hasOwnProperty(name) ?
os.attributeMap_[name] : name;
var substitution =
(os.attributeMap_[name]) ?
null : os.parseAttribute_(value);
if (substitution) {
if (outName == 'class') {
// Dynamically setting the @class attribute gets ignored by the
// browser. We need to set the .className property instead.
outName = '.className';
} else if (outName == 'style') {
// Similarly, on IE, setting the @style attribute has no effect.
// The cssText property of the style object must be set instead.
outName = '.style.cssText';
} else if (to.getAttribute(os.ATT_customtag)) {
// For custom tags, it is more useful to put values into properties
// where they can be accessed as objects, rather than placing them
// into attributes where they need to be serialized.
outName = '.' + outName;
} else if (os.isIe && !os.customAttributes_[outName] &&
outName.substring(0, 2).toLowerCase() == 'on') {
// For event handlers on IE, setAttribute doesn't work, so we need
// to create a function to set as a property.
outName = '.' + outName;
substitution = 'new Function(' + substitution + ')';
} else if (outName == 'selected' && to.tagName == 'OPTION') {
// For the @selected attribute of an option, set the property
// instead to allow false values to not mark the option selected.
outName = '.selected';
}
// TODO: reuse static array (IE6 perf).
if (!dynamicAttributes) {
dynamicAttributes = [];
}
dynamicAttributes.push(outName + ':' + substitution);
} else {
// For special attributes, do variable transformation.
if (os.attributeMap_.hasOwnProperty(name)) {
// If the attribute value looks like "${expr}", just use the "expr".
if (value.length > 3 &&
value.substring(0, 2) == '${' &&
value.charAt(value.length - 1) == '}') {
value = value.substring(2, value.length - 1);
}
// In special attributes, default value is empty array for repeats,
// null for others
value = os.transformExpression_(value,
name == 'repeat' ? os.VAR_emptyArray : 'null');
} else if (outName == 'class') {
// In IE, we must set className instead of class.
to.setAttribute('className', value);
} else if (outName == 'style') {
// Similarly, on IE, setting the @style attribute has no effect.
// The cssText property of the style object must be set instead.
to.style.cssText = value;
}
if (os.isIe && !os.customAttributes_.hasOwnProperty(outName) &&
outName.substring(0, 2).toLowerCase() == 'on') {
// In IE, setAttribute doesn't create event handlers, so we must
// use attachEvent in order to create handlers that are preserved
// by calls to cloneNode().
to.attachEvent(outName, new Function(value));
} else {
to.setAttribute(outName, value);
}
}
}
}
}
if (dynamicAttributes) {
os.appendJSTAttribute_(to, ATT_values, dynamicAttributes.join(';'));
}
};
/**
* Recursively compiles an individual node from XML to DOM (for JSTemplate)
* Special os.* tags and tags for which custom functions are defined
* are converted into markup recognizable by JSTemplate.
*
* TODO: process text nodes and attributes with ${} notation here
* @private
*/
os.compileNode_ = function(node) {
if (node.nodeType == DOM_TEXT_NODE) {
var textNode = node.cloneNode(false);
return os.breakTextNode_(textNode);
} else if (node.nodeType == DOM_ELEMENT_NODE) {
var output;
if (node.tagName.indexOf(':') > 0) {
if (node.tagName == 'os:Repeat') {
output = document.createElement(os.computeContainerTag_(node));
output.setAttribute(ATT_select, os.parseAttribute_(node.getAttribute('expression')));
var varAttr = node.getAttribute('var');
if (varAttr) {
os.appendJSTAttribute_(output, ATT_vars, varAttr + ': $this');
}
var contextAttr = node.getAttribute('context');
if (contextAttr) {
os.appendJSTAttribute_(output, ATT_vars, contextAttr + ': ' + VAR_loop);
}
os.appendJSTAttribute_(output, ATT_eval, 'os.setContextNode_($this, $context)');
} else if (node.tagName == 'os:If') {
output = document.createElement(os.computeContainerTag_(node));
output.setAttribute(ATT_display, os.parseAttribute_(node.getAttribute('condition')));
} else {
output = document.createElement('span');
output.setAttribute(os.ATT_customtag, node.tagName);
var custom = node.tagName.split(':');
os.appendJSTAttribute_(output, ATT_eval, 'os.doTag(this, \"'
+ custom[0] + '\", \"' + custom[1] + '\", $this, $context)');
var context = node.getAttribute('cur') || '{}';
output.setAttribute(ATT_innerselect, context);
// For os:Render, create a parent node reference.
// TODO: remove legacy support
if (node.tagName == 'os:render' || node.tagName == 'os:Render' ||
node.tagName == 'os:renderAll' || node.tagName == 'os:RenderAll') {
os.appendJSTAttribute_(output, ATT_values, os.VAR_parentnode + ':' +
os.VAR_node);
}
os.copyAttributes_(node, output, node.tagName);
}
} else {
output = os.xmlToHtml_(node);
}
if (output && !os.processTextContent_(node, output)) {
for (var child = node.firstChild; child; child = child.nextSibling) {
var compiledChild = os.compileNode_(child);
if (compiledChild) {
if (os.isArray(compiledChild)) {
for (var i = 0; i < compiledChild.length; i++) {
output.appendChild(compiledChild[i]);
}
} else {
// If inserting a TR into a TABLE, inject a TBODY element.
if (compiledChild.tagName == 'TR' && output.tagName == 'TABLE') {
var lastEl = output.lastChild;
while (lastEl && lastEl.nodeType != DOM_ELEMENT_NODE &&
lastEl.previousSibling) {
lastEl = lastEl.previousSibling;
}
if (!lastEl || lastEl.tagName != 'TBODY') {
lastEl = document.createElement('tbody');
output.appendChild(lastEl);
}
lastEl.appendChild(compiledChild);
} else {
output.appendChild(compiledChild);
}
}
}
}
}
return output;
}
return null;
};
/**
* Calculates the type of element best suited to encapsulating contents of a
* or tags. Inspects the element's children to see if one
* of the special cases should be used.
* "optgroup" for