All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 




© 2015 - 2025 Weber Informatics LLC | Privacy Policy