Fixes #61 Simplify parent/child relationships

This commit is contained in:
Patrick Steele-Idem 2015-04-22 09:53:41 -06:00
parent 374954254f
commit 183c3c62c4
41 changed files with 1574 additions and 874 deletions

View File

@ -1,297 +0,0 @@
/*
* 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.
*/
'use strict';
var forEachEntry = require('raptor-util').forEachEntry;
var ok = require('assert').ok;
var makeClass = require('raptor-util').makeClass;
function inheritProps(sub, sup) {
forEachEntry(sup, function (k, v) {
if (!sub[k]) {
sub[k] = v;
}
});
}
function Taglib(id) {
ok(id, '"id" expected');
this.id = id;
this.dirname = null;
this.tags = {};
this.textTransformers = [];
this.attributes = {};
this.patternAttributes = [];
this.inputFilesLookup = {};
this.imports = null;
}
Taglib.prototype = {
addInputFile: function(path) {
this.inputFilesLookup[path] = true;
},
getInputFiles: function() {
return Object.keys(this.inputFilesLookup);
},
addAttribute: function (attribute) {
if (attribute.pattern) {
this.patternAttributes.push(attribute);
} else if (attribute.name) {
this.attributes[attribute.name] = attribute;
} else {
throw new Error('Invalid attribute: ' + require('util').inspect(attribute));
}
},
getAttribute: function (name) {
var attribute = this.attributes[name];
if (!attribute) {
for (var i = 0, len = this.patternAttributes.length; i < len; i++) {
var patternAttribute = this.patternAttributes[i];
if (patternAttribute.pattern.test(name)) {
attribute = patternAttribute;
}
}
}
return attribute;
},
addTag: function (tag) {
ok(arguments.length === 1, 'Invalid args');
ok(tag.name, '"tag.name" is required');
this.tags[tag.name] = tag;
tag.taglibId = this.id;
},
addTextTransformer: function (transformer) {
this.textTransformers.push(transformer);
},
forEachTag: function (callback, thisObj) {
forEachEntry(this.tags, function (key, tag) {
callback.call(thisObj, tag);
}, this);
},
addImport: function(path) {
if (!this.imports) {
this.imports = [];
}
this.imports.push(path);
}
};
Taglib.Tag = makeClass({
$init: function(taglib) {
this.taglibId = taglib ? taglib.id : null;
this.renderer = null;
this.nodeClass = null;
this.template = null;
this.attributes = {};
this.transformers = {};
this.nestedVariables = null;
this.importedVariables = null;
this.patternAttributes = [];
this.bodyFunction = null;
},
inheritFrom: function (superTag) {
var subTag = this;
/*
* Have the sub tag inherit any properties from the super tag that are not in the sub tag
*/
forEachEntry(superTag, function (k, v) {
if (subTag[k] === undefined) {
subTag[k] = v;
}
});
[
'attributes',
'transformers',
'nestedVariables',
'importedVariables',
'bodyFunction'
].forEach(function (propName) {
inheritProps(subTag[propName], superTag[propName]);
});
subTag.patternAttributes = superTag.patternAttributes.concat(subTag.patternAttributes);
},
forEachVariable: function (callback, thisObj) {
if (!this.nestedVariables) {
return;
}
this.nestedVariables.vars.forEach(callback, thisObj);
},
forEachImportedVariable: function (callback, thisObj) {
if (!this.importedVariables) {
return;
}
forEachEntry(this.importedVariables, function (key, importedVariable) {
callback.call(thisObj, importedVariable);
});
},
forEachTransformer: function (callback, thisObj) {
forEachEntry(this.transformers, function (key, transformer) {
callback.call(thisObj, transformer);
});
},
hasTransformers: function () {
/*jshint unused:false */
for (var k in this.transformers) {
if (this.transformers.hasOwnProperty(k)) {
return true;
}
}
return false;
},
addAttribute: function (attr) {
if (attr.pattern) {
this.patternAttributes.push(attr);
} else {
if (attr.name === '*') {
attr.dynamicAttribute = true;
if (attr.targetProperty === null || attr.targetProperty === '') {
attr.targetProperty = null;
}
else if (!attr.targetProperty) {
attr.targetProperty = '*';
}
}
this.attributes[attr.name] = attr;
}
},
toString: function () {
return '[Tag: <' + this.name + '@' + this.taglibId + '>]';
},
forEachAttribute: function (callback, thisObj) {
for (var attrName in this.attributes) {
if (this.attributes.hasOwnProperty(attrName)) {
callback.call(thisObj, this.attributes[attrName]);
}
}
},
addNestedVariable: function (nestedVariable) {
if (!this.nestedVariables) {
this.nestedVariables = {
__noMerge: true,
vars: []
};
}
this.nestedVariables.vars.push(nestedVariable);
},
addImportedVariable: function (importedVariable) {
if (!this.importedVariables) {
this.importedVariables = {};
}
var key = importedVariable.targetProperty;
this.importedVariables[key] = importedVariable;
},
addTransformer: function (transformer) {
var key = transformer.path;
transformer.taglibId = this.taglibId;
this.transformers[key] = transformer;
},
setBodyFunction: function(name, params) {
this.bodyFunction = {
__noMerge: true,
name: name,
params: params
};
},
setBodyProperty: function(propertyName) {
this.bodyProperty = propertyName;
}
});
Taglib.Attribute = makeClass({
$init: function(name) {
this.name = name;
this.type = null;
this.required = false;
this.type = 'string';
this.allowExpressions = true;
this.setFlag = null;
}
});
Taglib.Property = makeClass({
$init: function() {
this.name = null;
this.type = 'string';
this.value = undefined;
}
});
Taglib.NestedVariable = makeClass({
$init: function() {
this.name = null;
}
});
Taglib.ImportedVariable = makeClass({
$init: function() {
this.targetProperty = null;
this.expression = null;
}
});
var nextTransformerId = 0;
Taglib.Transformer = makeClass({
$init: function() {
this.id = nextTransformerId++;
this.name = null;
this.tag = null;
this.path = null;
this.priority = null;
this._func = null;
this.properties = {};
},
getFunc: function () {
if (!this.path) {
throw new Error('Transformer path not defined for tag transformer (tag=' + this.tag + ')');
}
if (!this._func) {
var transformer = require(this.path);
if (typeof transformer === 'function') {
if (transformer.prototype.process) {
var Clazz = transformer;
var instance = new Clazz();
instance.id = this.id;
this._func = instance.process.bind(instance);
} else {
this._func = transformer;
}
} else {
this._func = transformer.process || transformer.transform;
}
}
return this._func;
},
toString: function () {
return '[Taglib.Transformer: ' + this.path + ']';
}
});
module.exports = Taglib;

View File

@ -0,0 +1,12 @@
var makeClass = require('raptor-util').makeClass;
module.exports = makeClass({
$init: function(name) {
this.name = name;
this.type = null;
this.required = false;
this.type = 'string';
this.allowExpressions = true;
this.setFlag = null;
}
});

View File

@ -0,0 +1,8 @@
var makeClass = require('raptor-util').makeClass;
module.exports = makeClass({
$init: function() {
this.targetProperty = null;
this.expression = null;
}
});

View File

@ -0,0 +1,7 @@
var makeClass = require('raptor-util').makeClass;
module.exports = makeClass({
$init: function() {
this.name = null;
}
});

View File

@ -0,0 +1,9 @@
var makeClass = require('raptor-util').makeClass;
module.exports = makeClass({
$init: function() {
this.name = null;
this.type = 'string';
this.value = undefined;
}
});

View File

@ -0,0 +1,167 @@
var makeClass = require('raptor-util').makeClass;
var forEachEntry = require('raptor-util').forEachEntry;
var ok = require('assert').ok;
function inheritProps(sub, sup) {
forEachEntry(sup, function (k, v) {
if (!sub[k]) {
sub[k] = v;
}
});
}
module.exports = makeClass({
$init: function(taglib) {
this.taglibId = taglib ? taglib.id : null;
this.renderer = null;
this.nodeClass = null;
this.template = null;
this.attributes = {};
this.transformers = {};
this.nestedVariables = null;
this.importedVariables = null;
this.patternAttributes = [];
this.bodyFunction = null;
this.nestedTags = null;
this.isRepeated = null;
this.isNestedTag = false;
this.parentTagName = null;
this.type = null; // Only applicable for nested tags
},
inheritFrom: function (superTag) {
var subTag = this;
/*
* Have the sub tag inherit any properties from the super tag that are not in the sub tag
*/
forEachEntry(superTag, function (k, v) {
if (subTag[k] === undefined) {
subTag[k] = v;
}
});
[
'attributes',
'transformers',
'nestedVariables',
'importedVariables',
'bodyFunction'
].forEach(function (propName) {
inheritProps(subTag[propName], superTag[propName]);
});
subTag.patternAttributes = superTag.patternAttributes.concat(subTag.patternAttributes);
},
forEachVariable: function (callback, thisObj) {
if (!this.nestedVariables) {
return;
}
this.nestedVariables.vars.forEach(callback, thisObj);
},
forEachImportedVariable: function (callback, thisObj) {
if (!this.importedVariables) {
return;
}
forEachEntry(this.importedVariables, function (key, importedVariable) {
callback.call(thisObj, importedVariable);
});
},
forEachTransformer: function (callback, thisObj) {
forEachEntry(this.transformers, function (key, transformer) {
callback.call(thisObj, transformer);
});
},
hasTransformers: function () {
/*jshint unused:false */
for (var k in this.transformers) {
if (this.transformers.hasOwnProperty(k)) {
return true;
}
}
return false;
},
addAttribute: function (attr) {
if (attr.pattern) {
this.patternAttributes.push(attr);
} else {
if (attr.name === '*') {
attr.dynamicAttribute = true;
if (attr.targetProperty === null || attr.targetProperty === '') {
attr.targetProperty = null;
}
else if (!attr.targetProperty) {
attr.targetProperty = '*';
}
}
this.attributes[attr.name] = attr;
}
},
toString: function () {
return '[Tag: <' + this.name + '@' + this.taglibId + '>]';
},
forEachAttribute: function (callback, thisObj) {
for (var attrName in this.attributes) {
if (this.attributes.hasOwnProperty(attrName)) {
callback.call(thisObj, this.attributes[attrName]);
}
}
},
addNestedVariable: function (nestedVariable) {
if (!this.nestedVariables) {
this.nestedVariables = {
__noMerge: true,
vars: []
};
}
this.nestedVariables.vars.push(nestedVariable);
},
addImportedVariable: function (importedVariable) {
if (!this.importedVariables) {
this.importedVariables = {};
}
var key = importedVariable.targetProperty;
this.importedVariables[key] = importedVariable;
},
addTransformer: function (transformer) {
var key = transformer.path;
transformer.taglibId = this.taglibId;
this.transformers[key] = transformer;
},
setBodyFunction: function(name, params) {
this.bodyFunction = {
__noMerge: true,
name: name,
params: params
};
},
setBodyProperty: function(propertyName) {
this.bodyProperty = propertyName;
},
addNestedTag: function(nestedTag) {
ok(nestedTag.name, '"nestedTag.name" is required');
if (!this.nestedTags) {
this.nestedTags = {};
}
nestedTag.isNestedTag = true;
this.nestedTags[nestedTag.name] = nestedTag;
},
forEachNestedTag: function (callback, thisObj) {
if (!this.nestedTags) {
return;
}
forEachEntry(this.nestedTags, function (key, nestedTag) {
callback.call(thisObj, nestedTag);
});
},
hasNestedTags: function() {
return this.nestedTags != null;
}
});

View File

@ -0,0 +1,42 @@
var makeClass = require('raptor-util').makeClass;
var nextTransformerId = 0;
module.exports = makeClass({
$init: function() {
this.id = nextTransformerId++;
this.name = null;
this.tag = null;
this.path = null;
this.priority = null;
this._func = null;
this.properties = {};
},
getFunc: function () {
if (!this.path) {
throw new Error('Transformer path not defined for tag transformer (tag=' + this.tag + ')');
}
if (!this._func) {
var transformer = require(this.path);
if (typeof transformer === 'function') {
if (transformer.prototype.process) {
var Clazz = transformer;
var instance = new Clazz();
instance.id = this.id;
this._func = instance.process.bind(instance);
} else {
this._func = transformer;
}
} else {
this._func = transformer.process || transformer.transform;
}
}
return this._func;
},
toString: function () {
return '[Taglib.Transformer: ' + this.path + ']';
}
});

View File

@ -0,0 +1,94 @@
/*
* 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.
*/
'use strict';
var forEachEntry = require('raptor-util').forEachEntry;
var ok = require('assert').ok;
function Taglib(id) {
ok(id, '"id" expected');
this.id = id;
this.dirname = null;
this.tags = {};
this.textTransformers = [];
this.attributes = {};
this.patternAttributes = [];
this.inputFilesLookup = {};
this.imports = null;
}
Taglib.prototype = {
addInputFile: function(path) {
this.inputFilesLookup[path] = true;
},
getInputFiles: function() {
return Object.keys(this.inputFilesLookup);
},
addAttribute: function (attribute) {
if (attribute.pattern) {
this.patternAttributes.push(attribute);
} else if (attribute.name) {
this.attributes[attribute.name] = attribute;
} else {
throw new Error('Invalid attribute: ' + require('util').inspect(attribute));
}
},
getAttribute: function (name) {
var attribute = this.attributes[name];
if (!attribute) {
for (var i = 0, len = this.patternAttributes.length; i < len; i++) {
var patternAttribute = this.patternAttributes[i];
if (patternAttribute.pattern.test(name)) {
attribute = patternAttribute;
}
}
}
return attribute;
},
addTag: function (tag) {
ok(arguments.length === 1, 'Invalid args');
ok(tag.name, '"tag.name" is required');
this.tags[tag.name] = tag;
tag.taglibId = this.id;
},
addTextTransformer: function (transformer) {
this.textTransformers.push(transformer);
},
forEachTag: function (callback, thisObj) {
forEachEntry(this.tags, function (key, tag) {
callback.call(thisObj, tag);
}, this);
},
addImport: function(path) {
if (!this.imports) {
this.imports = [];
}
this.imports.push(path);
}
};
Taglib.Tag = require('./Tag');
Taglib.Attribute = require('./Attribute');
Taglib.Property = require('./Property');
Taglib.NestedVariable = require('./NestedVariable');
Taglib.ImportedVariable = require('./ImportedVariable');
Taglib.Transformer = require('./Transformer');
module.exports = Taglib;

View File

@ -1,5 +1,7 @@
var ok = require('assert').ok;
var createError = require('raptor-util').createError;
var Taglib = require('./Taglib');
var extend = require('raptor-util/extend');
function transformerComparator(a, b) {
a = a.priority;
@ -66,7 +68,33 @@ function TaglibLookup() {
}
TaglibLookup.prototype = {
_mergeNestedTags: function(taglib) {
var Tag = Taglib.Tag;
// Loop over all of the nested tags and register a new custom tag
// with the fully qualified name
var merged = this.merged;
function handleNestedTag(nestedTag, parentTagName) {
var fullyQualifiedName = parentTagName + '.' + nestedTag.name;
// Create a clone of the nested tag since we need to add some new
// properties
var clonedNestedTag = new Tag();
extend(clonedNestedTag ,nestedTag);
// Record the fully qualified name of the parent tag that this
// custom tag is associated with.
clonedNestedTag.parentTagName = parentTagName;
clonedNestedTag.name = fullyQualifiedName;
merged.tags[fullyQualifiedName] = clonedNestedTag;
}
taglib.forEachTag(function(tag) {
tag.forEachNestedTag(function(nestedTag) {
handleNestedTag(nestedTag, tag.name);
});
});
},
addTaglib: function (taglib) {
ok(taglib, '"taglib" is required');
@ -79,6 +107,8 @@ TaglibLookup.prototype = {
this.taglibsById[taglib.id] = taglib;
merge(this.merged, taglib);
this._mergeNestedTags(taglib);
},
getTag: function (element) {

View File

@ -1,525 +0,0 @@
var fs ;
var req = require;
try {
fs = req('fs');
} catch(e) {
}
var ok = require('assert').ok;
var nodePath = require('path');
var Taglib = require('./Taglib');
var cache = {};
var forEachEntry = require('raptor-util').forEachEntry;
var raptorRegexp = require('raptor-regexp');
var tagDefFromCode = require('./tag-def-from-code');
var resolve = require('../util/resolve'); // NOTE: different implementation for browser
var propertyHandlers = require('property-handlers');
var jsonminify = require('jsonminify');
var safeVarName = /^[A-Za-z_$][A-Za-z0-9_]*$/;
var bodyFunctionRegExp = /^([A-Za-z_$][A-Za-z0-9_]*)(?:\(([^)]*)\))?$/;
function exists(path) {
try {
require.resolve(path);
return true;
} catch(e) {
return false;
}
}
function createDefaultTagDef() {
return {
attributes: {
'*': {
type: 'string',
targetProperty: null,
preserveName: false
}
}
};
}
function buildAttribute(attr, attrProps, path) {
propertyHandlers(attrProps, {
type: function(value) {
attr.type = value;
},
targetProperty: function(value) {
attr.targetProperty = value;
},
defaultValue: function(value) {
attr.defaultValue = value;
},
pattern: function(value) {
if (value === true) {
var patternRegExp = raptorRegexp.simple(attr.name);
attr.pattern = patternRegExp;
}
},
allowExpressions: function(value) {
attr.allowExpressions = value;
},
preserveName: function(value) {
attr.preserveName = value;
},
required: function(value) {
attr.required = value === true;
},
removeDashes: function(value) {
attr.removeDashes = value === true;
},
description: function() {
},
setFlag: function(value) {
attr.setFlag = value;
},
ignore: function(value) {
if (value === true) {
attr.ignore = true;
}
}
}, path);
return attr;
}
function handleAttributes(value, parent, path) {
forEachEntry(value, function(attrName, attrProps) {
var attr = new Taglib.Attribute(attrName);
if (attrProps == null) {
attrProps = {
type: 'string'
};
} else if (typeof attrProps === 'string') {
attrProps = {
type: attrProps
};
}
buildAttribute(attr, attrProps, '"' + attrName + '" attribute as part of ' + path);
parent.addAttribute(attr);
});
}
function buildTag(tagObject, path, taglib, dirname) {
ok(tagObject);
ok(typeof path === 'string');
ok(taglib);
ok(typeof dirname === 'string');
var tag = new Taglib.Tag(taglib);
if (tagObject.attributes == null) {
// allow any attributes if no attributes are declared
tagObject.attributes = {
'*': 'string'
};
}
propertyHandlers(tagObject, {
name: function(value) {
tag.name = value;
},
renderer: function(value) {
var path = resolve(value, dirname);
tag.renderer = path;
},
template: function(value) {
var path = nodePath.resolve(dirname, value);
if (!exists(path)) {
throw new Error('Template at path "' + path + '" does not exist.');
}
tag.template = path;
},
attributes: function(value) {
handleAttributes(value, tag, path);
},
nodeClass: function(value) {
var path = resolve(value, dirname);
tag.nodeClass = path;
},
preserveWhitespace: function(value) {
tag.preserveWhitespace = !!value;
},
transformer: function(value) {
var transformer = new Taglib.Transformer();
if (typeof value === 'string') {
value = {
path: value
};
}
propertyHandlers(value, {
path: function(value) {
var path = resolve(value, dirname);
transformer.path = 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);
},
'var': function(value) {
tag.addNestedVariable({
name: value
});
},
bodyFunction: function(value) {
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: <function-name>([param1, param2, ...])');
}
var functionName = parts[1];
var params = parts[2];
if (params) {
params = params.trim().split(/\s*,\s*/);
for (var i=0; i<params.length; i++) {
if (params[i].length === 0) {
throw new Error('Invalid parameters for body-function with value of "' + value + '"');
} else if (!safeVarName.test(params[i])) {
throw new Error('Invalid parameter name of "' + params[i] + '" for body-function with value of "' + value + '"');
}
}
} else {
params = [];
}
tag.setBodyFunction(functionName, params);
},
bodyProperty: function(value) {
tag.setBodyProperty(value);
},
vars: function(value) {
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);
});
}
},
importVar: function(value) {
forEachEntry(value, function(varName, varValue) {
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 = expression;
tag.addImportedVariable(importedVar);
});
}
}, path);
return tag;
}
/**
* @param {String} tagsConfigPath path to tag definition file
* @param {String} tagsConfigDirname path to directory of tags config file (should be path.dirname(tagsConfigPath))
* @param {String|Object} dir the path to directory to scan
* @param {String} taglib the taglib that is being loaded
*/
function scanTagsDir(tagsConfigPath, tagsConfigDirname, dir, taglib) {
var prefix;
if (typeof dir === 'object') {
prefix = dir.prefix;
dir = dir.path;
}
if (prefix == null) {
// no prefix by default
prefix = '';
}
dir = nodePath.resolve(tagsConfigDirname, dir);
var children = fs.readdirSync(dir);
var rendererJSFile;
for (var i=0, len=children.length; i<len; i++) {
rendererJSFile = null;
var childFilename = children[i];
if (childFilename === 'node_modules') {
continue;
}
var tagName = prefix + childFilename;
var tagDirname = nodePath.join(dir, childFilename);
var tagFile = nodePath.join(dir, childFilename, 'marko-tag.json');
var tag = null;
var rendererFile = nodePath.join(dir, childFilename, 'renderer.js');
var indexFile = nodePath.join(dir, childFilename, 'index.js');
var templateFile = nodePath.join(dir, childFilename, 'template.marko');
var tagDef = null;
// Record dependencies so that we can check if a template is up-to-date
taglib.addInputFile(tagFile);
taglib.addInputFile(rendererFile);
if (fs.existsSync(tagFile)) {
// marko-tag.json exists in the directory, use that as the tag definition
tagDef = JSON.parse(jsonminify(fs.readFileSync(tagFile, {encoding: 'utf8'})));
if (!tagDef.renderer && !tagDef.template) {
if (fs.existsSync(rendererFile)) {
tagDef.renderer = rendererFile;
} else if (fs.existsSync(indexFile)) {
tagDef.renderer = indexFile;
} else if (fs.existsSync(templateFile)) {
tagDef.template = templateFile;
} else if (fs.existsSync(templateFile + ".html")) {
tagDef.template = templateFile + ".html";
} else {
throw new Error('Invalid tag file: ' + tagFile + '. Neither a renderer or a template was found for tag.');
}
}
tag = buildTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tag.name || tagName;
taglib.addTag(tag);
} else {
// marko-tag.json does *not* exist... checking for a 'renderer.js'
if (fs.existsSync(rendererFile)) {
rendererJSFile = rendererFile;
} else if (fs.existsSync(indexFile)) {
rendererJSFile = indexFile;
} else {
var exTemplateFile;
if (fs.existsSync(templateFile)) {
exTemplateFile = templateFile;
}
else if (fs.existsSync(templateFile + ".html")){
exTemplateFile = templateFile + ".html";
}
if(exTemplateFile){
var templateCode = fs.readFileSync(exTemplateFile, {encoding: 'utf8'});
tagDef = tagDefFromCode.extractTagDef(templateCode);
if (!tagDef) {
tagDef = createDefaultTagDef();
}
tagDef.template = exTemplateFile;
}
}
if (rendererJSFile) {
var rendererCode = fs.readFileSync(rendererJSFile, {encoding: 'utf8'});
tagDef = tagDefFromCode.extractTagDef(rendererCode);
if (!tagDef) {
tagDef = createDefaultTagDef();
}
tagDef.renderer = rendererJSFile;
tag = buildTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tagName;
taglib.addTag(tag);
}
if (tagDef) {
tag = buildTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tagName;
taglib.addTag(tag);
}
}
}
}
function load(path) {
if (cache[path]) {
return cache[path];
}
var taglibObject;
try {
taglibObject = require(path);
} catch(e) {
throw new Error('Unable to parse taglib JSON at path "' + path + '". Exception: ' + e);
}
var taglib = new Taglib(path);
taglib.addInputFile(path);
var dirname = nodePath.dirname(path);
propertyHandlers(taglibObject, {
attributes: function(value) {
handleAttributes(value, taglib, path);
},
tags: function(tags) {
forEachEntry(tags, function(tagName, path) {
ok(path, 'Invalid tag definition for "' + tagName + '"');
var tagObject;
var tagDirname;
if (typeof path === 'string') {
path = nodePath.resolve(dirname, path);
taglib.addInputFile(path);
tagDirname = nodePath.dirname(path);
if (!exists(path)) {
throw new Error('Tag at path "' + path + '" does not exist. Taglib: ' + taglib.id);
}
try {
tagObject = require(path);
} catch(e) {
throw new Error('Unable to parse tag JSON for tag at path "' + path + '"');
}
} else {
tagDirname = dirname; // Tag is in the same taglib file
tagObject = path;
path = '<' + tagName + '> tag in ' + taglib.id;
}
var tag = buildTag(tagObject, path, taglib, tagDirname);
if (tag.name === undefined) {
tag.name = tagName;
}
taglib.addTag(tag);
});
},
tagsDir: function(dir) {
if (Array.isArray(dir)) {
for (var i = 0; i < dir.length; i++) {
scanTagsDir(path, dirname, dir[i], taglib);
}
} else {
scanTagsDir(path, dirname, dir, taglib);
}
},
taglibImports: function(imports) {
if (imports && Array.isArray(imports)) {
for (var i=0; i<imports.length; i++) {
var curImport = imports[i];
if (typeof curImport === 'string') {
var basename = nodePath.basename(curImport);
if (basename === 'package.json') {
var packagePath = resolve(curImport, dirname);
var pkg = require(packagePath);
var dependencies = pkg.dependencies;
if (dependencies) {
var dependencyNames = Object.keys(dependencies);
for (var j=0; j<dependencyNames.length; j++) {
var dependencyName = dependencyNames[j];
var importPath;
try {
importPath = require('resolve-from')(dirname, dependencyName + '/marko-taglib.json');
} catch(e) {}
if (importPath) {
taglib.addImport(importPath);
}
}
}
}
}
}
}
},
textTransformer: function(value) {
var transformer = new Taglib.Transformer();
if (typeof value === 'string') {
value = {
path: value
};
}
propertyHandlers(value, {
path: function(value) {
var path = resolve(value, dirname);
transformer.path = path;
}
}, 'text-transformer in ' + path);
ok(transformer.path, '"path" is required for transformer');
taglib.addTextTransformer(transformer);
}
}, path);
taglib.id = taglib.path = path;
cache[path] = taglib;
return taglib;
}
exports.load = load;

View File

@ -0,0 +1,16 @@
var ok = require('assert').ok;
var forEachEntry = require('raptor-util').forEachEntry;
var loader = require('./loader');
module.exports = function handleAttributes(value, parent, path) {
ok(parent);
forEachEntry(value, function(attrName, attrProps) {
var attr = loader.attributeLoader.loadAttribute(
attrName,
attrProps,
'"' + attrName + '" attribute as part of ' + path);
parent.addAttribute(attr);
});
};

View File

@ -0,0 +1,20 @@
require('raptor-polyfill/string/startsWith');
var loader = require('./loader');
var cache = {};
function load(path) {
if (cache[path]) {
return cache[path];
}
var taglib = loader.taglibLoader.loadTaglib(path);
cache[path] = taglib;
return taglib;
}
exports.load = load;

View File

@ -0,0 +1,83 @@
var assert = require('assert');
var raptorRegexp = require('raptor-regexp');
var propertyHandlers = require('property-handlers');
var Taglib = require('../Taglib');
function AttrHandlers(attr){
assert.ok(attr);
assert.equal(typeof attr, 'object');
this.attr = attr;
}
AttrHandlers.prototype = {
type: function(value) {
var attr = this.attr;
attr.type = value;
},
targetProperty: function(value) {
var attr = this.attr;
attr.targetProperty = value;
},
defaultValue: function(value) {
var attr = this.attr;
attr.defaultValue = value;
},
pattern: function(value) {
var attr = this.attr;
if (value === true) {
var patternRegExp = raptorRegexp.simple(attr.name);
attr.pattern = patternRegExp;
}
},
allowExpressions: function(value) {
var attr = this.attr;
attr.allowExpressions = value;
},
preserveName: function(value) {
var attr = this.attr;
attr.preserveName = value;
},
required: function(value) {
var attr = this.attr;
attr.required = value === true;
},
removeDashes: function(value) {
var attr = this.attr;
attr.removeDashes = value === true;
},
description: function() {
},
setFlag: function(value) {
var attr = this.attr;
attr.setFlag = value;
},
ignore: function(value) {
var attr = this.attr;
if (value === true) {
attr.ignore = true;
}
}
};
exports.isSupportedProperty = function(name) {
return AttrHandlers.prototype.hasOwnProperty(name);
};
exports.loadAttribute = function loadAttribute(attrName, attrProps, path) {
var attr = new Taglib.Attribute(attrName);
if (attrProps == null) {
attrProps = {
type: 'string'
};
} else if (typeof attrProps === 'string') {
attrProps = {
type: attrProps
};
}
var attrHandlers = new AttrHandlers(attr);
propertyHandlers(attrProps, attrHandlers, path);
return attr;
};

View File

@ -0,0 +1,392 @@
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');
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 TagHandlers(tag, dirname, path) {
this.tag = tag;
this.dirname = dirname;
this.path = path;
}
TagHandlers.prototype = {
name: function(value) {
var tag = this.tag;
tag.name = value;
},
renderer: function(value) {
var tag = this.tag;
var dirname = this.dirname;
var path = resolve(value, dirname);
tag.renderer = path;
},
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.');
}
tag.template = path;
},
attributes: function(value) {
var tag = this.tag;
var path = this.path;
handleAttributes(value, tag, path);
},
nodeClass: function(value) {
var tag = this.tag;
var dirname = this.dirname;
var path = resolve(value, dirname);
tag.nodeClass = path;
},
preserveWhitespace: function(value) {
var tag = this.tag;
tag.preserveWhitespace = !!value;
},
transformer: function(value) {
var tag = this.tag;
var dirname = this.dirname;
var path = this.path;
var transformer = new Taglib.Transformer();
if (typeof value === 'string') {
value = {
path: value
};
}
propertyHandlers(value, {
path: function(value) {
var path = resolve(value, dirname);
transformer.path = 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);
},
'var': function(value) {
var tag = this.tag;
tag.addNestedVariable({
name: value
});
},
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: <function-name>([param1, param2, ...])');
}
var functionName = parts[1];
var params = parts[2];
if (params) {
params = params.trim().split(/\s*,\s*/);
for (var i=0; i<params.length; i++) {
if (params[i].length === 0) {
throw new Error('Invalid parameters for body-function with value of "' + value + '"');
} else if (!safeVarName.test(params[i])) {
throw new Error('Invalid parameter name of "' + params[i] + '" for body-function with value of "' + value + '"');
}
}
} else {
params = [];
}
tag.setBodyFunction(functionName, params);
},
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);
});
}
},
importVar: function(value) {
var tag = this.tag;
forEachEntry(value, function(varName, varValue) {
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 = expression;
tag.addImportedVariable(importedVar);
});
},
type: function(value) {
var tag = this.tag;
tag.type = value;
},
nestedTags: function(value) {
}
};
exports.isSupportedProperty = function(name) {
return TagHandlers.prototype.hasOwnProperty(name);
};
function loadTag(tagProps, path, taglib, dirname) {
ok(tagProps);
ok(typeof path === 'string');
ok(taglib);
ok(typeof dirname === 'string');
var tag = new Taglib.Tag(taglib);
if (tagProps.attributes == null) {
// allow any attributes if no attributes are declared
tagProps.attributes = {
'*': 'string'
};
}
var tagHandlers = new TagHandlers(tag, dirname, path);
// We add a handler 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
// "<nested_tag_name>"
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<parts.length; i++) {
part = parts[i];
if (part.startsWith('@')) {
hasAttr = true;
if (i === 0) {
// Use the first attribute value as the name of the target property
nestedTagTargetProperty = part.substring(1);
}
} else if (part.startsWith('<')) {
hasNestedTag = true;
} else {
// Unmatched property that is not an attribute or a
// nested tag
return false;
}
}
var attrProps = {};
var tagProps = {};
var k;
if (value != null && typeof value === 'object') {
for (k in value) {
if (value.hasOwnProperty(k)) {
if (k.startsWith('@') || k.startsWith('<')) {
// Move over all of the attributes and nested tags
// to the tag definition.
tagProps[k] = value[k];
delete value[k];
} else {
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
// and attribute
tagProps[k] = value[k];
attrProps[k] = value[k];
delete value[k];
} else if (loader.tagLoader.isSupportedProperty(propNameDashes)) {
// Move over all of the properties that are associated with a tag
tagProps[k] = value[k];
delete value[k];
} else if (loader.attributeLoader.isSupportedProperty(propNameDashes)) {
// Move over all of the properties that are associated with an attr
attrProps[k] = value[k];
delete value[k];
}
}
}
}
// If there are any left over properties then something is wrong
// with the user's taglib.
if (!isObjectEmpty(value)) {
throw new Error('Unsupported properties of [' +
Object.keys(value).join(', ') +
'] for "' + name + '" in "' + path + '"');
}
var type = attrProps.type;
if (!type && hasAttr && hasNestedTag) {
// If we have an attribute and a nested tag then default
// the attribute type to "expression"
attrProps.type = 'expression';
}
} else if (typeof value === 'string') {
if (hasNestedTag && hasAttr) {
tagProps = attrProps = {
type: value
};
} else if (hasNestedTag) {
tagProps = {
type: value
};
} else {
attrProps = {
type: value
};
}
}
// Now that we have separated out attribute properties and tag properties
// we need to create the actual attributes and nested tags
for (i=0; i<parts.length; i++) {
part = parts[i];
if (part.startsWith('@')) {
// This is a shorthand attribute
var attrName = part.substring(1);
var attr = loader.attributeLoader.loadAttribute(
attrName,
attrProps,
'"' + attrName + '" attribute as part of ' + path);
tag.addAttribute(attr);
} else if (part.startsWith('<')) {
// This is a shorthand nested tag
var nestedTag = loadTag(
tagProps,
name + ' of ' + path,
taglib,
dirname);
var isNestedTagRepeated = false;
if (part.endsWith('[]')) {
isNestedTagRepeated = true;
part = part.slice(0, -2);
}
var nestedTagName = part.substring(1, part.length-1);
nestedTag.name = nestedTagName;
nestedTag.isRepeated = isNestedTagRepeated;
// Use the name of the attribute as the target property unless
// this target property was explicitly provided
nestedTag.targetProperty = attrProps.targetProperty || nestedTagTargetProperty;
tag.addNestedTag(nestedTag);
} else {
return false;
}
}
};
propertyHandlers(tagProps, tagHandlers, path);
return tag;
}
exports.loadTag = loadTag;

View File

@ -0,0 +1,187 @@
var ok = require('assert').ok;
var nodePath = require('path');
var handleAttributes = require('./handleAttributes');
var scanTagsDir = require('./scanTagsDir');
var resolve = require('../../util/resolve'); // NOTE: different implementation for browser
var propertyHandlers = require('property-handlers');
var Taglib = require('../Taglib');
var taglibReader = require('./taglib-reader');
var loader = require('./loader');
function exists(path) {
try {
require.resolve(path);
return true;
} catch(e) {
return false;
}
}
function handleTag(taglibHandlers, tagName, path) {
var taglib = taglibHandlers.taglib;
var dirname = taglibHandlers.dirname;
ok(path, 'Invalid tag definition for "' + tagName + '"');
var tagObject;
var tagDirname;
if (typeof path === 'string') {
path = nodePath.resolve(dirname, path);
taglib.addInputFile(path);
tagDirname = nodePath.dirname(path);
if (!exists(path)) {
throw new Error('Tag at path "' + path + '" does not exist. Taglib: ' + taglib.id);
}
try {
tagObject = require(path);
} catch(e) {
throw new Error('Unable to parse tag JSON for tag at path "' + path + '"');
}
} else {
tagDirname = dirname; // Tag is in the same taglib file
tagObject = path;
path = '<' + tagName + '> tag in ' + taglib.id;
}
var tag = loader.tagLoader.loadTag(tagObject, path, taglib, tagDirname);
if (tag.name === undefined) {
tag.name = tagName;
}
taglib.addTag(tag);
}
function TaglibHandlers(taglib, path) {
ok(taglib);
ok(path);
this.taglib = taglib;
this.path = path;
this.dirname = nodePath.dirname(path);
}
TaglibHandlers.prototype = {
attributes: function(value) {
var taglib = this.taglib;
var path = this.path;
handleAttributes(value, taglib, path);
},
tags: function(tags) {
for (var tagName in tags) {
if (tags.hasOwnProperty(tagName)) {
handleTag(this, tagName, tags[tagName]);
}
}
},
tagsDir: function(dir) {
var taglib = this.taglib;
var path = this.path;
var dirname = this.dirname;
if (Array.isArray(dir)) {
for (var i = 0; i < dir.length; i++) {
scanTagsDir(path, dirname, dir[i], taglib);
}
} else {
scanTagsDir(path, dirname, dir, taglib);
}
},
taglibImports: function(imports) {
var taglib = this.taglib;
var dirname = this.dirname;
if (imports && Array.isArray(imports)) {
for (var i=0; i<imports.length; i++) {
var curImport = imports[i];
if (typeof curImport === 'string') {
var basename = nodePath.basename(curImport);
if (basename === 'package.json') {
var packagePath = resolve(curImport, dirname);
var pkg = require(packagePath);
var dependencies = pkg.dependencies;
if (dependencies) {
var dependencyNames = Object.keys(dependencies);
for (var j=0; j<dependencyNames.length; j++) {
var dependencyName = dependencyNames[j];
var importPath;
try {
importPath = require('resolve-from')(dirname, dependencyName + '/marko-taglib.json');
} catch(e) {}
if (importPath) {
taglib.addImport(importPath);
}
}
}
}
}
}
}
},
textTransformer: function(value) {
var taglib = this.taglib;
var path = this.path;
var dirname = this.dirname;
var transformer = new Taglib.Transformer();
if (typeof value === 'string') {
value = {
path: value
};
}
propertyHandlers(value, {
path: function(value) {
var path = resolve(value, dirname);
transformer.path = path;
}
}, 'text-transformer in ' + path);
ok(transformer.path, '"path" is required for transformer');
taglib.addTextTransformer(transformer);
},
'*': function(name, value) {
var taglib = this.taglib;
var path = this.path;
if (name.startsWith('<')) {
handleTag(this, name.slice(1, -1), value);
} else if (name.startsWith('@')) {
var attrName = name.substring(1);
var attr = loader.attributeLoader.loadAttribute(
attrName,
value,
'"' + attrName + '" attribute as part of ' + path);
taglib.addAttribute(attr);
} 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);
taglib.id = taglib.path = path;
return taglib;
};

View File

@ -0,0 +1,3 @@
exports.taglibLoader = require('./loader-taglib');
exports.tagLoader = require('./loader-tag');
exports.attributeLoader = require('./loader-attribute');

View File

@ -0,0 +1,6 @@
{
"browser": {
"./scanTagsDir.js": "./scanTagsDir-browser.js",
"./taglib-reader.js": "./taglib-reader-browser.js"
}
}

View File

@ -0,0 +1,3 @@
module.exports = function scanTagsDir() {
// no-op in the browser
};

View File

@ -0,0 +1,128 @@
var nodePath = require('path');
var fs = require('fs');
var jsonminify = require('jsonminify');
var tagDefFromCode = require('./tag-def-from-code');
var loader = require('./loader');
function createDefaultTagDef() {
return {
attributes: {
'*': {
type: 'string',
targetProperty: null,
preserveName: false
}
}
};
}
/**
* @param {String} tagsConfigPath path to tag definition file
* @param {String} tagsConfigDirname path to directory of tags config file (should be path.dirname(tagsConfigPath))
* @param {String|Object} dir the path to directory to scan
* @param {String} taglib the taglib that is being loaded
*/
module.exports = function scanTagsDir(tagsConfigPath, tagsConfigDirname, dir, taglib) {
var prefix;
if (typeof dir === 'object') {
prefix = dir.prefix;
dir = dir.path;
}
if (prefix == null) {
// no prefix by default
prefix = '';
}
dir = nodePath.resolve(tagsConfigDirname, dir);
var children = fs.readdirSync(dir);
var rendererJSFile;
for (var i=0, len=children.length; i<len; i++) {
rendererJSFile = null;
var childFilename = children[i];
if (childFilename === 'node_modules') {
continue;
}
var tagName = prefix + childFilename;
var tagDirname = nodePath.join(dir, childFilename);
var tagFile = nodePath.join(dir, childFilename, 'marko-tag.json');
var tag = null;
var rendererFile = nodePath.join(dir, childFilename, 'renderer.js');
var indexFile = nodePath.join(dir, childFilename, 'index.js');
var templateFile = nodePath.join(dir, childFilename, 'template.marko');
var tagDef = null;
// Record dependencies so that we can check if a template is up-to-date
taglib.addInputFile(tagFile);
taglib.addInputFile(rendererFile);
if (fs.existsSync(tagFile)) {
// marko-tag.json exists in the directory, use that as the tag definition
tagDef = JSON.parse(jsonminify(fs.readFileSync(tagFile, {encoding: 'utf8'})));
if (!tagDef.renderer && !tagDef.template) {
if (fs.existsSync(rendererFile)) {
tagDef.renderer = rendererFile;
} else if (fs.existsSync(indexFile)) {
tagDef.renderer = indexFile;
} else if (fs.existsSync(templateFile)) {
tagDef.template = templateFile;
} else if (fs.existsSync(templateFile + ".html")) {
tagDef.template = templateFile + ".html";
} else {
throw new Error('Invalid tag file: ' + tagFile + '. Neither a renderer or a template was found for tag.');
}
}
tag = loader.tagLoader.loadTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tag.name || tagName;
taglib.addTag(tag);
} else {
// marko-tag.json does *not* exist... checking for a 'renderer.js'
if (fs.existsSync(rendererFile)) {
rendererJSFile = rendererFile;
} else if (fs.existsSync(indexFile)) {
rendererJSFile = indexFile;
} else {
var exTemplateFile;
if (fs.existsSync(templateFile)) {
exTemplateFile = templateFile;
}
else if (fs.existsSync(templateFile + ".html")){
exTemplateFile = templateFile + ".html";
}
if(exTemplateFile){
var templateCode = fs.readFileSync(exTemplateFile, {encoding: 'utf8'});
tagDef = tagDefFromCode.extractTagDef(templateCode);
if (!tagDef) {
tagDef = createDefaultTagDef();
}
tagDef.template = exTemplateFile;
}
}
if (rendererJSFile) {
var rendererCode = fs.readFileSync(rendererJSFile, {encoding: 'utf8'});
tagDef = tagDefFromCode.extractTagDef(rendererCode);
if (!tagDef) {
tagDef = createDefaultTagDef();
}
tagDef.renderer = rendererJSFile;
tag = loader.tagLoader.loadTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tagName;
taglib.addTag(tag);
}
if (tagDef) {
tag = loader.tagLoader.loadTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tagName;
taglib.addTag(tag);
}
}
}
};

View File

@ -0,0 +1,11 @@
exports.readTaglib = function (path) {
var taglibProps;
try {
taglibProps = require(path);
} catch(e) {
throw new Error('Unable to parse taglib JSON at path "' + path + '". Exception: ' + e);
}
return taglibProps;
};

View File

@ -0,0 +1,13 @@
var fs = require('fs');
var jsonminify = require('jsonminify');
exports.readTaglib = function (path) {
var json = fs.readFileSync(path, 'utf8');
try {
var taglibProps = JSON.parse(jsonminify(json));
return taglibProps;
} catch(e) {
throw new Error('Unable to parse taglib at path "' + path + '". Error: ' + e);
}
};

View File

@ -14,7 +14,9 @@
"scripts": {
"test": "node_modules/.bin/mocha --ui bdd --reporter spec ./test && node_modules/.bin/jshint compiler/ runtime/ taglibs/",
"test-fast": "node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-test",
"test-async": "node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-async-test"
"test-async": "node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-async-test",
"test-taglib-loader": "node_modules/.bin/mocha --ui bdd --reporter spec ./test/taglib-loader-test",
"jshint": "node_modules/.bin/jshint compiler/ runtime/ taglibs/"
},
"author": "Patrick Steele-Idem <pnidem@gmail.com>",
"maintainers": [

View File

@ -155,30 +155,61 @@ module.exports = {
/**
* Invoke a tag handler render function
*/
t: function (out, renderFunc, input, body, hasOutParam) {
t: function (out, renderFunc, input, renderBody, options) {
if (!input) {
input = {};
}
if (body) {
input.renderBody = body;
input.invokeBody = function() {
if (!WARNED_INVOKE_BODY) {
WARNED_INVOKE_BODY = 1;
logger.warn('invokeBody(...) deprecated. Use renderBody(out) instead.', new Error().stack);
}
var hasOutParam;
var targetProperty;
var parent;
var hasNestedTags;
var isRepeated;
if (!hasOutParam) {
var args = arrayFromArguments(arguments);
args.unshift(out);
body.apply(this, args);
} else {
body.apply(this, arguments);
}
};
if (options) {
hasOutParam = options.hasOutParam;
parent = options.parent;
targetProperty = options.targetProperty;
hasNestedTags = options.hasNestedTags;
isRepeated = options.isRepeated;
}
renderFunc(input, out);
if (renderBody) {
if (hasNestedTags) {
renderBody(out, input);
} else {
input.renderBody = renderBody;
input.invokeBody = function() {
if (!WARNED_INVOKE_BODY) {
WARNED_INVOKE_BODY = 1;
logger.warn('invokeBody(...) deprecated. Use renderBody(out) instead.', new Error().stack);
}
if (!hasOutParam) {
var args = arrayFromArguments(arguments);
args.unshift(out);
renderBody.apply(this, args);
} else {
renderBody.apply(this, arguments);
}
};
}
}
if (renderFunc) {
renderFunc(input, out);
} else if (targetProperty) {
if (isRepeated) {
var existingArray = parent[targetProperty];
if (existingArray) {
existingArray.push(input);
} else {
parent[targetProperty] = [input];
}
} else {
parent[targetProperty] = input;
}
}
},
c: function (out, func) {
var output = out.captureString(func);

View File

@ -70,6 +70,28 @@ function getPropsStr(props, template) {
return '{}';
}
}
function getNextNestedTagVarName(template) {
if (template.data.nextNestedTagId == null) {
template.data.nextNestedTagId = 0;
}
return '__nestedTagInput' + (template.data.nextNestedTagId++);
}
function getNestedTagParentNode(nestedTagNode, tag) {
var parentTagName = tag.parentTagName;
var currentNode = nestedTagNode.parentNode;
while (currentNode) {
if (currentNode.localName === parentTagName) {
return currentNode;
}
currentNode = currentNode.parentNode;
}
}
function TagHandlerNode(tag) {
if (!this.nodeType) {
TagHandlerNode.$super.call(this);
@ -112,13 +134,41 @@ TagHandlerNode.prototype = {
doGenerateCode: function (template) {
template.addStaticVar('__renderer', '__helpers.r');
var _this = this;
var rendererPath = template.getRequirePath(this.tag.renderer); // Resolve a path to the renderer relative to the directory of the template
var handlerVar = addHandlerVar(template, rendererPath);
var tagHelperVar = template.addStaticVar('__tag', '__helpers.t');
var bodyFunction = this.tag.bodyFunction;
var bodyProperty = this.tag.bodyProperty;
var tag = this.tag;
this.tag.forEachImportedVariable(function (importedVariable) {
var rendererPath;
var handlerVar;
if (tag.renderer) {
rendererPath = template.getRequirePath(this.tag.renderer); // Resolve a path to the renderer relative to the directory of the template
handlerVar = addHandlerVar(template, rendererPath);
}
var bodyFunction = tag.bodyFunction;
var bodyProperty = tag.bodyProperty;
var isNestedTag = tag.isNestedTag === true;
var hasNestedTags = tag.hasNestedTags();
var tagHelperVar = template.addStaticVar('__tag', '__helpers.t');
var nestedTagVar;
var nestedTagParentNode = null;
if (isNestedTag) {
nestedTagParentNode = getNestedTagParentNode(this, tag);
if (nestedTagParentNode == null) {
this.addError('Parent tag of <' + tag.parentTagName + '> not found in template.');
return;
}
nestedTagVar = nestedTagParentNode.data.nestedTagVar;
}
if (hasNestedTags) {
nestedTagVar = this.data.nestedTagVar = getNextNestedTagVarName(template);
}
tag.forEachImportedVariable(function (importedVariable) {
this.setProperty(importedVariable.targetProperty, template.makeExpression(importedVariable.expression));
}, this);
@ -138,7 +188,7 @@ TagHandlerNode.prototype = {
var variableNames = [];
_this.tag.forEachVariable(function (nestedVar) {
tag.forEachVariable(function (nestedVar) {
var varName;
if (nestedVar.nameFromAttribute) {
var possibleNameAttributes = nestedVar.nameFromAttribute.split(/\s+or\s+|\s*,\s*/i);
@ -178,7 +228,7 @@ TagHandlerNode.prototype = {
template.functionCall(tagHelperVar, function () {
template.code('out,\n').indent(function () {
template.line(handlerVar + ',').indent();
template.line((handlerVar ? handlerVar : 'null') + ',').indent();
if (_this.dynamicAttributes) {
template.indent(function() {
@ -215,32 +265,56 @@ TagHandlerNode.prototype = {
template.code(propsCode);
if (_this.hasChildren() && !_this.tag.bodyFunction) {
var bodyParams = [];
var hasOutParam = false;
var hasOutParam = false;
variableNames.forEach(function (varName) {
if (varName === 'out') {
hasOutParam = true;
}
bodyParams.push(varName);
});
if (_this.hasChildren() && !tag.bodyFunction) {
var bodyParams = [];
if (hasNestedTags) {
bodyParams.push(nestedTagVar);
} else {
variableNames.forEach(function (varName) {
if (varName === 'out') {
hasOutParam = true;
}
bodyParams.push(varName);
});
}
var params;
if (hasOutParam) {
params = bodyParams.join(',');
} else {
params = 'out' + (bodyParams.length ? ',' + bodyParams.join(',') : '');
params = 'out' + (bodyParams.length ? ', ' + bodyParams.join(', ') : '');
}
template.code(',\n').line('function(' + params + ') {').indent(function () {
_this.generateCodeForChildren(template);
}).indent().code('}');
}
if (hasNestedTags || isNestedTag || hasOutParam) {
var options = [];
if (hasNestedTags) {
options.push('hasNestedTags: 1');
}
if (hasOutParam) {
template.code(',\n').code(template.indentStr() + '1');
options.push('hasOutParam: 1');
}
if (isNestedTag) {
options.push('targetProperty: ' + JSON.stringify(tag.targetProperty));
options.push('parent: ' + nestedTagVar);
if (tag.isRepeated) {
options.push('isRepeated: 1');
}
}
template.code(',\n').code(template.indentStr() + '{ ' + options.join(', ') + ' }');
}
});
});

View File

@ -269,24 +269,23 @@ module.exports = function transform(node, compiler, template) {
node.setPreserveWhitespace(true);
}
if (tag.renderer || tag.template) {
if (tag.renderer || tag.isNestedTag) {
shouldRemoveAttr = false;
if (tag.renderer) {
//Instead of compiling as a static XML element, we'll
//make the node render as a tag handler node so that
//writes code that invokes the handler
TagHandlerNode.convertNode(node, tag);
if (inputAttr) {
node.setInputExpression(template.makeExpression(inputAttr));
}
} else {
var templatePath = compiler.getRequirePath(tag.template);
// The tag is mapped to a template that will be used to perform
// the rendering so convert the node into a "IncludeNode" that can
// be used to include the output of rendering a template
IncludeNode.convertNode(node, templatePath);
//Instead of compiling as a static XML element, we'll
//make the node render as a tag handler node so that
//writes code that invokes the handler
TagHandlerNode.convertNode(node, tag);
if (inputAttr) {
node.setInputExpression(template.makeExpression(inputAttr));
}
} else if (tag.template) {
shouldRemoveAttr = false;
var templatePath = compiler.getRequirePath(tag.template);
// The tag is mapped to a template that will be used to perform
// the rendering so convert the node into a "IncludeNode" that can
// be used to include the output of rendering a template
IncludeNode.convertNode(node, templatePath);
} else if (tag.nodeClass) {
shouldRemoveAttr = false;

View File

@ -116,6 +116,8 @@
"template": "./taglib/test-circular-template-b/template.marko"
}
},
"<test-nested-tags-tabs>": "./taglib/test-nested-tags-tabs/marko-tag.json",
"<test-nested-tags-overlay>": "./taglib/test-nested-tags-overlay/marko-tag.json",
"tags-dir": "./taglib/scanned-tags",
"taglib-imports": ["./package.json"]
}

View File

@ -0,0 +1,25 @@
{
"<shorthand-checkbox>": {
"@label <label>": "string",
"@checked": "boolean",
"<checked>": "boolean"
},
"<shorthand-overlay>": {
"nested-tags": {
"body": {
"@condensed": "boolean"
}
}
},
"tags": {
"shorthand-button": {
"@label": "string"
},
"shorthand-tabs": {
"@tabs <tab>[]": {
"@label": "string"
},
"@orientation": "string"
}
}
}

View File

@ -0,0 +1,21 @@
{
"renderer": "./renderer",
"@header <header>": {
"@class": {
"type": "string",
"target-property": "className"
}
},
"@body <body>": {
"@class": {
"type": "string",
"target-property": "className"
}
},
"@footer <footer>": {
"@class": {
"type": "string",
"target-property": "className"
}
}
}

View File

@ -0,0 +1,15 @@
var marko = require('../../../../');
var template = marko.load(require.resolve('./template.marko'));
exports.renderer = function(input, out) {
var header = input.header;
var body = input.body;
var footer = input.footer;
template.render({
header: header,
body: body,
footer: footer
}, out);
};

View File

@ -0,0 +1,13 @@
<div class="overlay">
<div class="overlay-header ${data.header.className}" if="data.header">
<invoke function="data.header.renderBody(out)"/>
</div>
<div class="overlay-body ${data.body.className}" if="data.body">
<invoke function="data.body.renderBody(out)"/>
</div>
<div class="overlay-footer ${data.footer.className}" if="data.footer">
<invoke function="data.footer.renderBody(out)"/>
</div>
</div>

View File

@ -0,0 +1,7 @@
{
"renderer": "./renderer",
"@orientation": "string",
"@tabs <tab>[]": {
"@title": "string"
}
}

View File

@ -0,0 +1,11 @@
var marko = require('../../../../');
var template = marko.load(require.resolve('./template.marko'));
exports.renderer = function(input, out) {
var tabs = input.tabs;
template.render({
tabs: tabs
}, out);
};

View File

@ -0,0 +1,14 @@
<var name="tabs" value="data.tabs"/>
<div class="tabs">
<ul>
<li for="tab in tabs">
${tab.title}
</li>
</ul>
<div class="tab-content">
<div class="tab" for="tab in tabs">
<invoke function="tab.renderBody(out)"/>
</div>
</div>
</div>

View File

@ -0,0 +1 @@
<div class="tabs"><ul><li>Tab 1</li><li>Tab 3</li></ul><div class="tab-content"><div class="tab">Tab 1 content</div><div class="tab">Tab 3 content</div></div></div>

View File

@ -0,0 +1,14 @@
<test-nested-tags-tabs>
<test-nested-tags-tabs.tab title="Tab 1">
Tab 1 content
</test-nested-tags-tabs.tab>
<test-nested-tags-tabs.tab title="Tab 2" if="false">
Tab 2 content
</test-nested-tags-tabs.tab>
<test-nested-tags-tabs.tab title="Tab 3">
Tab 3 content
</test-nested-tags-tabs.tab>
</test-nested-tags-tabs>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -0,0 +1 @@
<div class="overlay"><div class="overlay-header ">Header content!</div><div class="overlay-body my-body">Body content</div><div class="overlay-footer my-footer">Footer content</div></div>

View File

@ -0,0 +1,10 @@
<test-nested-tags-overlay header="data.header">
<test-nested-tags-overlay.body class="my-body">
Body content
</test-nested-tags-overlay.body>
<test-nested-tags-overlay.footer class="my-footer">
Footer content
</test-nested-tags-overlay.footer>
</test-nested-tags-overlay>

View File

@ -0,0 +1,7 @@
exports.templateData = {
header: {
renderBody: function(out) {
out.write('Header content!');
}
}
};

View File

@ -0,0 +1,43 @@
'use strict';
var chai = require('chai');
chai.Assertion.includeStack = true;
require('chai').should();
var expect = require('chai').expect;
var nodePath = require('path');
describe('taglib-loader' , function() {
beforeEach(function(done) {
for (var k in require.cache) {
if (require.cache.hasOwnProperty(k)) {
delete require.cache[k];
}
}
done();
});
it('should load a taglib with shorthand attributes and tags', function() {
var taglibLoader = require('../compiler/taglibs').loader;
var taglib = taglibLoader.load(nodePath.join(__dirname, 'fixtures/taglib-shorthand/marko-taglib.json'));
expect(taglib != null).to.equal(true);
var shorthandCheckbox = taglib.tags['shorthand-checkbox'];
expect(shorthandCheckbox.attributes.checked.type).to.equal('boolean');
expect(shorthandCheckbox.attributes.label.type).to.equal('string');
expect(shorthandCheckbox.nestedTags.label.type).to.equal('string');
expect(shorthandCheckbox.nestedTags.checked.type).to.equal('boolean');
var shorthandTabsTag = taglib.tags['shorthand-tabs'];
expect(shorthandTabsTag.attributes.orientation != null).to.equal(true);
expect(shorthandTabsTag.attributes.orientation.type).to.equal('string');
expect(shorthandTabsTag.attributes.tabs.type).to.equal('expression');
var nestedTabTag = shorthandTabsTag.nestedTags.tab;
expect(nestedTabTag.attributes.label != null).to.equal(true);
expect(nestedTabTag.isRepeated).to.equal(true);
expect(nestedTabTag.targetProperty).to.equal('tabs');
});
});