/* * Copyright 2011 eBay Software Foundation * * Licensed 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. */ require('raptor-polyfill/string/startsWith'); var ok = require('assert').ok; var Taglib = require('./Taglib'); var propertyHandlers = require('property-handlers'); var isObjectEmpty = require('raptor-util/isObjectEmpty'); var nodePath = require('path'); var resolve = require('../util/resolve'); // NOTE: different implementation for browser var ok = require('assert').ok; var bodyFunctionRegExp = /^([A-Za-z_$][A-Za-z0-9_]*)(?:\(([^)]*)\))?$/; var safeVarName = /^[A-Za-z_$][A-Za-z0-9_]*$/; var handleAttributes = require('./handleAttributes'); var Taglib = require('./Taglib'); var propertyHandlers = require('property-handlers'); var forEachEntry = require('raptor-util').forEachEntry; var loader = require('./loader'); var markoCompiler = require('../'); function exists(path) { try { require.resolve(path); return true; } catch(e) { return false; } } function removeDashes(str) { return str.replace(/-([a-z])/g, function (match, lower) { return lower.toUpperCase(); }); } function handleVar(tag, value, path) { var nestedVariable; if (typeof value === 'string') { nestedVariable = { name: value }; } else { nestedVariable = {}; propertyHandlers(value, { name: function(value) { nestedVariable.name = value; }, nameFromAttribute: function(value) { nestedVariable.nameFromAttribute = value; } }, path); if (!nestedVariable.name && !nestedVariable.nameFromAttribute) { throw new Error('The "name" or "name-from-attribute" attribute is required for a nested variable'); } } tag.addNestedVariable(nestedVariable); } /** * We load tag definition using this class. Properties in the taglib * definition (which is just a JavaScript object with properties) * are mapped to handler methods in an instance of this type. * * @param {Tag} tag The initially empty Tag instance that we populate * @param {String} dirname The full file system path associated with the tag being loaded * @param {String} path An informational path associated with this tag (used for error reporting) */ function TagHandlers(tag, dirname, path, taglib) { this.tag = tag; this.dirname = dirname; this.path = path; this.taglib = taglib; if (!taglib) { throw new Error('taglib expected'); } } TagHandlers.prototype = { /** * The tag name * @param {String} value The tag name */ name: function(value) { var tag = this.tag; tag.name = value; }, /** * The path to the renderer JS module to use for this tag. * * NOTE: We use the equivalent of require.resolve to resolve the JS module * and use the tag directory as the "from". * * @param {String} value The renderer path */ renderer: function(value) { var tag = this.tag; var dirname = this.dirname; var path = resolve(value, dirname); this.taglib.addInputFile(path); tag.renderer = path; }, /** * A tag can use a renderer or a template to do the rendering. If * a template is provided then the value should be the path to the * template to use to render the custom tag. */ template: function(value) { var tag = this.tag; var dirname = this.dirname; var path = nodePath.resolve(dirname, value); if (!exists(path)) { throw new Error('Template at path "' + path + '" does not exist.'); } this.taglib.addInputFile(path); tag.template = path; }, /** * An Object where each property maps to an attribute definition. * The property key will be the attribute name and the property value * will be the attribute definition. Example: * { * "attributes": { * "foo": "string", * "bar": "expression" * } * } */ attributes: function(value) { var tag = this.tag; var path = this.path; handleAttributes(value, tag, path); }, /** * A custom tag can be mapped to module that is is used * to generate compile-time code for the custom tag. A * node type is created based on the methods and methods * exported by the code codegen module. */ codeGenerator: function(value) { var tag = this.tag; var dirname = this.dirname; var path = resolve(value, dirname); tag.codeGeneratorModulePath = path; this.taglib.addInputFile(path); }, /** * A custom tag can be mapped to a compile-time Node that gets * added to the parsed Abstract Syntax Tree (AST). The Node can * then generate custom JS code at compile time. The value * should be a path to a JS module that gets resolved using the * equivalent of require.resolve(path) */ nodeFactory: function(value) { var tag = this.tag; var dirname = this.dirname; var path = resolve(value, dirname); tag.nodeFactoryPath = path; this.taglib.addInputFile(path); }, /** * If the "preserve-whitespace" property is set to true then * all whitespace nested below the custom tag in a template * will be stripped instead of going through the normal whitespace * removal rules. */ preserveWhitespace: function(value) { var tag = this.tag; tag.preserveWhitespace = !!value; }, /** * If a custom tag has an associated transformer then the transformer * will be called on the compile-time Node. The transformer can manipulate * the AST using the DOM-like API to change how the code gets generated. */ transformer: function(value) { var tag = this.tag; var dirname = this.dirname; var path = this.path; var taglib = this.taglib; var transformer = new Taglib.Transformer(); if (typeof value === 'string') { // The value is a simple string type // so treat the value as the path to the JS // module for the transformer value = { path: value }; } /** * The transformer is a complex type and we need * to process each property to load the Transformer * definition. */ propertyHandlers(value, { path: function(value) { var path = resolve(value, dirname); transformer.path = path; taglib.addInputFile(path); }, priority: function(value) { transformer.priority = value; }, name: function(value) { transformer.name = value; }, properties: function(value) { var properties = transformer.properties || (transformer.properties = {}); for (var k in value) { if (value.hasOwnProperty(k)) { properties[k] = value[k]; } } } }, 'transformer in ' + path); ok(transformer.path, '"path" is required for transformer'); tag.addTransformer(transformer); }, /** * The "var" property is used to declared nested variables that get * added as JavaScript variables at compile time. * * Examples: * * "var": "myScopedVariable", * * "var": { * "name": "myScopedVariable" * } * * "var": { * "name-from-attribute": "var" * } */ 'var': function(value) { handleVar(this.tag, value, '"var" in tag ' + this.path); }, /** * The "vars" property is equivalent to the "var" property * except that it expects an array of nested variables. */ vars: function(value) { var tag = this.tag; var self = this; if (value) { value.forEach(function(v, i) { handleVar(tag, v, '"vars"[' + i + '] in tag ' + self.path); }); } }, /** * The "body-function" property" allows the nested body content to be mapped * to a function at compile time. The body function gets mapped to a property * of the tag renderer at render time. The body function can have any number * of parameters. * * Example: * - "body-function": "_handleBody(param1, param2, param3)" */ bodyFunction: function(value) { var tag = this.tag; var parts = bodyFunctionRegExp.exec(value); if (!parts) { throw new Error('Invalid value of "' + value + '" for "body-function". Expected value to be of the following form: ([param1, param2, ...])'); } var functionName = parts[1]; var params = parts[2]; if (params) { params = params.trim().split(/\s*,\s*/); for (var i=0; i" tagHandlers['*'] = function(name, value) { var parts = name.split(/\s+|\s+[,]\s+/); var i; var part; var hasNestedTag = false; var hasAttr = false; var nestedTagTargetProperty = null; // We do one pass to figure out if there is an // attribute or nested tag or both for (i=0; i