'use strict'; var ok = require('assert').ok; 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 propertyHandlers = require('property-handlers'); var forEachEntry = require('raptor-util/forEachEntry'); var markoCompiler = require('../'); var createError = require('raptor-util/createError'); var types = require('./types'); var attributeLoader = require('./loader-attribute'); var DependencyChain = require('./DependencyChain'); 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 hasAttributes(tagProps) { if (tagProps.attributes != null) { return true; } for (var name in tagProps) { if (tagProps.hasOwnProperty(name) && name.startsWith('@')) { return true; } } return false; } /** * 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) */ class TagLoader { constructor(dependencyChain) { this.dependencyChain = dependencyChain; this.filePath = null; this.tag = null; this.dirname = null; } _load(tagProps, filePath) { this.tag = new types.Tag(); this.filePath = filePath; this.dirname = nodePath.dirname(filePath); var tag = this.tag; tag.filePath = filePath; tag.dir = this.dirname; if (!hasAttributes(tagProps)) { // allow any attributes if no attributes are declared tagProps.attributes = { '*': { type: 'string', targetProperty: null, preserveName: false } }; } propertyHandlers(tagProps, this, this.dependencyChain.toString()); return tag; } _handleVar(value, dependencyChain) { var tag = this.tag; 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; } }, dependencyChain.toString()); if (!nestedVariable.name && !nestedVariable.nameFromAttribute) { throw new Error('The "name" or "name-from-attribute" attribute is required for a nested variable (' + dependencyChain + ')'); } } tag.addNestedVariable(nestedVariable); } /** * This is handler is for any properties that didn't match * one of the default property handlers. This is used to * match properties in the form of "@attr_name" or * "" */ '*'(name, value) { var tag = this.tag; var dependencyChain = this.dependencyChain; 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 { this._handleVar(v, this.dependencyChain.append('vars[' + i + ']')); }); } } /** * 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(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 { var importedVar = { targetProperty: varName }; var expression = varValue; if (!expression) { expression = varName; } else if (typeof expression === 'object') { expression = expression.expression; } if (!expression) { throw new Error('Invalid "import-var": ' + require('util').inspect(varValue)); } importedVar.expression = markoCompiler.builder.parseExpression(expression); tag.addImportedVariable(importedVar); }); } /** * The tag type. */ type(value) { var tag = this.tag; tag.type = value; } /** * Declare a nested tag. * * Example: * { * ... * "nested-tags": { * "tab": { * "target-property": "tabs", * "isRepeated": true * } * } * } */ nestedTags(value) { var filePath = this.filePath; var tag = this.tag; forEachEntry(value, (nestedTagName, nestedTagDef) => { var dependencyChain = this.dependencyChain.append(`nestedTags["${nestedTagName}]`); var nestedTag = loadTag( nestedTagDef, filePath, dependencyChain); nestedTag.name = nestedTagName; tag.addNestedTag(nestedTag); if (!nestedTag.isRepeated) { let attr = attributeLoader.loadAttribute( nestedTag.targetProperty, { type: 'object' }, dependencyChain); tag.addAttribute(attr); } }); } escapeXmlBody(value) { if (value === false) { this.tag.escapeXmlBody = false; } } /** * Sends the body content type. This is used to control how the body * content is parsed. */ body(value) { if (value === 'static-text' || value === 'parsed-text' || value === 'html') { this.tag.body = value; } else { throw new Error('Invalid value for "body". Allowed: "static-text", "parsed-text" or "html"'); } } openTagOnly(value) { this.tag.openTagOnly = value; } noOutput(value) { this.tag.noOutput = value; } autocomplete(value) { this.tag.autocomplete = value; } parseOptions(value) { this.tag.parseOptions = value; } deprecated(value) { this.tag.deprecated = value; } parseAttributes(value) { this.tag.parseAttributes = value; } } function isSupportedProperty(name) { return TagLoader.prototype.hasOwnProperty(name); } function loadTag(tagProps, filePath, dependencyChain) { ok(typeof tagProps === 'object', 'Invalid "tagProps"'); ok(typeof filePath === 'string'); if (!dependencyChain) { dependencyChain = new DependencyChain([filePath]); } var tagLoader = new TagLoader(dependencyChain); try { return tagLoader._load(tagProps, filePath); } catch(err) { throw createError('Unable to load tag (' + dependencyChain + '): ' + err, err); } } exports.loadTag = loadTag; exports.isSupportedProperty = isSupportedProperty;