diff --git a/compiler/TypeConverter.js b/compiler/TypeConverter.js index 05577de45..09035c1af 100644 --- a/compiler/TypeConverter.js +++ b/compiler/TypeConverter.js @@ -30,9 +30,7 @@ TypeConverter.convert = function (value, targetType, allowExpressions) { return value; } - - - if (targetType === 'expression') { + if (targetType === 'expression' || targetType === 'object' || targetType === 'array') { if (value === '') { value = 'null'; } diff --git a/compiler/taglibs/Taglib/Tag.js b/compiler/taglibs/Taglib/Tag.js index 8433f623c..5f8df415f 100644 --- a/compiler/taglibs/Taglib/Tag.js +++ b/compiler/taglibs/Taglib/Tag.js @@ -150,6 +150,10 @@ module.exports = makeClass({ nestedTag.isNestedTag = true; + if (!nestedTag.targetProperty) { + nestedTag.targetProperty = nestedTag.name; + } + this.nestedTags[nestedTag.name] = nestedTag; }, forEachNestedTag: function (callback, thisObj) { diff --git a/compiler/taglibs/taglib-loader/loader-attribute.js b/compiler/taglibs/taglib-loader/loader-attribute.js index fc33eca34..6709e982d 100644 --- a/compiler/taglibs/taglib-loader/loader-attribute.js +++ b/compiler/taglibs/taglib-loader/loader-attribute.js @@ -10,18 +10,50 @@ function AttrHandlers(attr){ } AttrHandlers.prototype = { + /** + * The attribute type. One of the following: + * - string (the default) + * - expression (a JavaScript expression) + * - number + * - integer + * - int + * - boolean + * - float + * - double + * - object + * - array + * + */ type: function(value) { var attr = this.attr; attr.type = value; }, + + /** + * The name of the target property to use when mapping + * the attribute to a property on the target object. + */ targetProperty: function(value) { var attr = this.attr; attr.targetProperty = value; }, + /** + * The "default-value" property allows a default value + * to be provided when the attribute is not declared + * on the custom tag. + */ defaultValue: function(value) { var attr = this.attr; attr.defaultValue = value; }, + /** + * The "pattern" property allows the attribute + * to be matched based on a simplified regular expression. + * + * Example: + * + * "pattern": "myprefix-*" + */ pattern: function(value) { var attr = this.attr; if (value === true) { @@ -29,29 +61,80 @@ AttrHandlers.prototype = { attr.pattern = patternRegExp; } }, + + /** + * If "allow-expressions" is set to true (the default) then + * the the attribute value will be parsed to find any dynamic + * parts. + */ allowExpressions: function(value) { var attr = this.attr; attr.allowExpressions = value; }, + + /** + * By default, the Marko compiler maps an attribute + * to a property by removing all dashes from the attribute + * name and converting each character after a dash to + * an uppercase character (e.g. "my-attr" --> "myAttr"). + * + * Setting "preserve-name" to true will prevent this from + * happening for the attribute. + */ preserveName: function(value) { var attr = this.attr; attr.preserveName = value; }, + /** + * Declares an attribute as required. Currently, this is + * not enforced and is only used for documentation purposes. + * + * Example: + * "required": true + */ required: function(value) { var attr = this.attr; attr.required = value === true; }, + /** + * This is the opposite of "preserve-name" and will result + * in dashes being removed from the attribute if set to true. + */ removeDashes: function(value) { var attr = this.attr; attr.removeDashes = value === true; }, + /** + * The description of the attribute. Only used for documentation. + */ description: function() { }, + + /** + * The "set-flag" property allows a "flag" to be added to a Node instance + * at compile time if the attribute is found on the node. This is helpful + * if an attribute uses a pattern and a transformer wants to have a simple + * check to see if the Node has an attribute that matched the pattern. + * + * Example: + * + * "set-flag": "myCustomFlag" + * + * A Node instance can be checked if it has a flag set as shown below: + * + * if (node.hasFlag('myCustomFlag')) { ... } + * + * + */ setFlag: function(value) { var attr = this.attr; attr.setFlag = value; }, + /** + * An attribute can be marked for ignore. Ignored attributes + * will be ignored during compilation. + */ ignore: function(value) { var attr = this.attr; if (value === true) { diff --git a/compiler/taglibs/taglib-loader/loader-tag.js b/compiler/taglibs/taglib-loader/loader-tag.js index 479d599bc..ab988b08e 100644 --- a/compiler/taglibs/taglib-loader/loader-tag.js +++ b/compiler/taglibs/taglib-loader/loader-tag.js @@ -28,19 +28,71 @@ function removeDashes(str) { }); } +function handleVar(tag, value, path) { + var nestedVariable; -function TagHandlers(tag, dirname, path) { + 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; } 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; @@ -48,6 +100,12 @@ TagHandlers.prototype = { 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; @@ -59,12 +117,32 @@ TagHandlers.prototype = { 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 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) + */ nodeClass: function(value) { var tag = this.tag; var dirname = this.dirname; @@ -72,10 +150,22 @@ TagHandlers.prototype = { var path = resolve(value, dirname); tag.nodeClass = 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; @@ -84,11 +174,19 @@ TagHandlers.prototype = { 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); @@ -119,12 +217,48 @@ TagHandlers.prototype = { 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) { - var tag = this.tag; - tag.addNestedVariable({ - name: 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); @@ -149,44 +283,27 @@ TagHandlers.prototype = { tag.setBodyFunction(functionName, params); }, + /** + * The "body-property" property can be used to map the body content + * to a String property on the renderer's input object. + * + * Example: + * "body-property": "label" + */ bodyProperty: function(value) { var tag = this.tag; tag.setBodyProperty(value); }, - vars: function(value) { - var tag = this.tag; - if (value) { - value.forEach(function(v, i) { - var nestedVariable; - - if (typeof v === 'string') { - nestedVariable = { - name: v - }; - } else { - nestedVariable = {}; - - propertyHandlers(v, { - - name: function(value) { - nestedVariable.name = value; - }, - - nameFromAttribute: function(value) { - nestedVariable.nameFromAttribute = value; - } - - }, 'var at index ' + i); - - if (!nestedVariable.name && !nestedVariable.nameFromAttribute) { - throw new Error('The "name" or "name-from-attribute" attribute is required for a nested variable'); - } - } - - tag.addNestedVariable(nestedVariable); - }); - } - }, + /** + * The "import-var" property can be used to add a property to the + * input object of the tag renderer whose value is determined by + * a JavaScript expression. + * + * Example: + * "import-var": { + * "myTargetProperty": "data.myCompileTimeJavaScriptExpression", + * } + */ importVar: function(value) { var tag = this.tag; forEachEntry(value, function(varName, varValue) { @@ -211,12 +328,42 @@ TagHandlers.prototype = { tag.addImportedVariable(importedVar); }); }, + /** + * The tag type. + */ type: function(value) { var tag = this.tag; tag.type = value; }, + /** + * Declare a nested tag. + * + * Example: + * { + * ... + * "nested-tags": { + * "tab": { + * "target-property": "tabs", + * "isRepeated": true + * } + * } + * } + */ nestedTags: function(value) { + var tagPath = this.path; + var taglib = this.taglib; + var dirname = this.dirname; + var tag = this.tag; + forEachEntry(value, function(nestedTagName, nestedTagDef) { + var nestedTag = loadTag( + nestedTagDef, + nestedTagName + ' of ' + tagPath, + taglib, + dirname); + nestedTag.name = nestedTagName; + tag.addNestedTag(nestedTag); + }); } }; @@ -239,7 +386,7 @@ function loadTag(tagProps, path, taglib, dirname) { }; } - var tagHandlers = new TagHandlers(tag, dirname, path); + var tagHandlers = new TagHandlers(tag, dirname, path, taglib); // We add a handler for any properties that didn't match // one of the default property handlers. This is used to @@ -288,10 +435,12 @@ function loadTag(tagProps, path, taglib, dirname) { tagProps[k] = value[k]; delete value[k]; } else { + // The property is not a shorthand attribute or shorthand + // tag so move it over to either the tag definition + // or the attribute definition or both the tag definition + // and attribute definition. var propNameDashes = removeDashes(k); - - if (loader.tagLoader.isSupportedProperty(propNameDashes) && loader.attributeLoader.isSupportedProperty(propNameDashes)) { // Move over all of the properties that are associated with a tag @@ -365,6 +514,8 @@ function loadTag(tagProps, path, taglib, dirname) { taglib, dirname); + // We use the '[]' suffix to indicate that a nested tag + // can be repeated var isNestedTagRepeated = false; if (part.endsWith('[]')) { isNestedTagRepeated = true; diff --git a/compiler/taglibs/taglib-loader/loader-taglib.js b/compiler/taglibs/taglib-loader/loader-taglib.js index b978e66be..3461ddea8 100644 --- a/compiler/taglibs/taglib-loader/loader-taglib.js +++ b/compiler/taglibs/taglib-loader/loader-taglib.js @@ -55,6 +55,15 @@ function handleTag(taglibHandlers, tagName, path) { taglib.addTag(tag); } +/** + * We load a taglib definion 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 {Taglib} taglib The initially empty Taglib instance that we will populate + * @param {String} path The file system path to the taglib that we are loading + */ function TaglibHandlers(taglib, path) { ok(taglib); ok(path); @@ -66,12 +75,40 @@ function TaglibHandlers(taglib, path) { TaglibHandlers.prototype = { attributes: function(value) { + // The value of the "attributes" property will be an object + // where each property maps to an attribute definition. Since these + // attributes are on the taglib they will be "global" attribute + // defintions. + // + // The property key will be the attribute name and the property value + // will be the attribute definition. Example: + // { + // "attributes": { + // "foo": "string", + // "bar": "expression" + // } + // } var taglib = this.taglib; var path = this.path; handleAttributes(value, taglib, path); }, tags: function(tags) { + // The value of the "tags" property will be an object + // where each property maps to an attribute definition. The property + // key will be the tag name and the property value + // will be the tag definition. Example: + // { + // "tags": { + // "foo": { + // "attributes": { ... } + // }, + // "bar": { + // "attributes": { ... } + // }, + // } + // } + for (var tagName in tags) { if (tags.hasOwnProperty(tagName)) { handleTag(this, tagName, tags[tagName]); @@ -79,6 +116,12 @@ TaglibHandlers.prototype = { } }, tagsDir: function(dir) { + // The "tags-dir" property is used to supporting scanning + // of a directory to discover custom tags. Scanning a directory + // is a much simpler way for a developer to create custom tags. + // Only one tag is allowed per directory and the directory name + // corresponds to the tag name. We only search for directories + // one level deep. var taglib = this.taglib; var path = this.path; var dirname = this.dirname; @@ -93,6 +136,14 @@ TaglibHandlers.prototype = { }, taglibImports: function(imports) { + // The "taglib-imports" property allows another taglib to be imported + // into this taglib so that the tags defined in the imported taglib + // will be part of this taglib. + // + // NOTE: If a taglib import refers to a package.json file then we read + // the package.json file and automatically import *all* of the + // taglibs from the installed modules found in the "dependencies" + // section var taglib = this.taglib; var dirname = this.dirname; @@ -127,6 +178,8 @@ TaglibHandlers.prototype = { }, textTransformer: function(value) { + // Marko allows a "text-transformer" to be registered. The provided + // text transformer will be called for any static text found in a template. var taglib = this.taglib; var path = this.path; var dirname = this.dirname; @@ -150,8 +203,20 @@ TaglibHandlers.prototype = { ok(transformer.path, '"path" is required for transformer'); taglib.addTextTransformer(transformer); - }, - '*': function(name, value) { + } +}; + +exports.loadTaglib = function(path) { + var taglibProps = taglibReader.readTaglib(path); + + var taglib = new Taglib(path); + taglib.addInputFile(path); + + var taglibHandlers = new TaglibHandlers(taglib, path); + + // We register a wildcard handler to handle "@my-attr" and "" + // properties (shorthand syntax) + taglibHandlers['*'] = function(name, value) { var taglib = this.taglib; var path = this.path; @@ -169,16 +234,7 @@ TaglibHandlers.prototype = { } else { return false; } - } -}; - -exports.loadTaglib = function(path) { - var taglibProps = taglibReader.readTaglib(path); - - var taglib = new Taglib(path); - taglib.addInputFile(path); - - var taglibHandlers = new TaglibHandlers(taglib, path); + }; propertyHandlers(taglibProps, taglibHandlers, path); diff --git a/test/fixtures/taglib/test-nested-tags-overlay/marko-tag.json b/test/fixtures/taglib/test-nested-tags-overlay/marko-tag.json index 634e7eed9..9686a762c 100644 --- a/test/fixtures/taglib/test-nested-tags-overlay/marko-tag.json +++ b/test/fixtures/taglib/test-nested-tags-overlay/marko-tag.json @@ -12,10 +12,12 @@ "target-property": "className" } }, - "@footer