Skip to content

Commit

Permalink
Merge pull request #94 from kethinov/0.4.1
Browse files Browse the repository at this point in the history
closes #93
  • Loading branch information
kethinov authored Feb 6, 2017
2 parents d7da840 + 2a64d2d commit f688f7e
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 40 deletions.
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,9 @@ Using Teddy in Node.js

Teddy is designed for use with [Express](http://expressjs.com) in [Node.js](http://nodejs.org).

- First require the node modules `express`, and `teddy`
- Then initialize express and configure it to your liking
- In your express config, make sure to include this line: `app.engine('html', teddy.__express)`
- First require the node modules `express`, and `teddy`.
- Then initialize express and configure it to your liking.
- In your express config, make sure to include this line: `app.engine('html', teddy.__express)`.

If you're looking for a more fully-featured web framework to build web apps with using Teddy templates, then try out Teddy's companion, [Roosevelt](https://github.com/kethinov/roosevelt).

Expand Down Expand Up @@ -403,12 +403,23 @@ API documentation
- `1`: The default. Concise logging. Usually just logs serious errors.
- `2`: Verbose logging. Logs even minor errors.
- `3`: Debug mode. Very verbose.
- `teddy.maxPasses(n)`: Sets the maximum number of passes the parser can execute over your template. If this maximum is exceeded, Teddy will stop attempting to render the template. The limit exists to prevent the possibility of teddy producing infinite loops due to improperly coded templates.
- Default: 25000.
- `teddy.cacheRenders(true/false)`: When this setting is enabled, Teddy will cache all unique combinations of templates and models. Any time a template has been rendered before with the given model, the cached template will be returned instead, improving performance.
- Default is false. *(Feature is currently experimental.)*
- `teddy.setDefaultCaches(n)`: Set the default number of unique caches to store per template when template caching is enabled.
- Default: 1.
- `teddy.setMaxCaches(template, n)`: Set the maximum number of unique caches to store for a given template when template caching is enabled.
- `teddy.setCacheWhitelist({'templateNameOrPath': maxCaches})`: Establish a whitelist of templates to cache and set their maxCache value.
- Example: `{'one.html': 1, 'two.html': 50, 'three.html': 250}`.
- The above example will only allow caching on one.html, two.html. and three.html. No other template renders will be cached.
- Also one.html will have a maximum unique cache count of 1, two.html's maximum will be 50, and three.html's maximum will be 250.
- Note: mutually exclusive with `teddy.setCacheBlacklist`
- `teddy.setCacheBlacklist(templateArray)`: Establish a blacklist of templates to never cache.
- Example: `['one.html', 'two.html', 'three.html']`.
- Note: mutually exclusive with `teddy.setCacheWhitelist`.
- `teddy.flushCache(template)`: Delete all the caches of a given template by supplying either its source code or a file name (in Node.js).
- `teddy.flushCache(template, model)`: Delete the cache of a specific template and model combination by supplying the template's source code or file name (in Node.js) along with the desired model to match.
- `teddy.maxPasses(n)`: Sets the maximum number of passes the parser can execute over your template. If this maximum is exceeded, Teddy will stop attempting to render the template. The limit exists to prevent the possibility of teddy producing infinite loops due to improperly coded templates.
- Default: 25000.
- `teddy.compileAtEveryRender(true/false)`: When this setting is enabled, Teddy will compile the template at each render rather than caching previous compiles.
- Default is false. *(Not recommended to enable in production for performance reasons.)*
- `teddy.minify(true/false)`: When this setting is enabled, Teddy will minify templates using its own internal minifier during the compile step.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "teddy",
"description": "Teddy Templating Engine",
"author": "Eric Newport <[email protected]>",
"version": "0.4.0",
"version": "0.4.1",
"homepage": "https://github.com/kethinov/teddy",
"license": "CC-BY-4.0",
"main": "teddy.js",
Expand Down
113 changes: 82 additions & 31 deletions teddy.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
verbosity: 1,
templateRoot: './',
cacheRenders: false,
defaultCaches: 1,
templateMaxCaches: {},
cacheWhitelist: false,
cacheBlacklist: [],
compileAtEveryRender: false,
minify: false,
maxPasses: 25000
Expand Down Expand Up @@ -140,6 +144,30 @@
teddy.params.cacheRenders = Boolean(v);
},

// mutator method to set default caches param: the number of cached versions of each templates to store by default if cacheRenders is enabled
setDefaultCaches: function(v) {
teddy.params.defaultCaches = parseInt(v);
},

// mutator method to set max caches for a given registered template
setMaxCaches: function(template, v) {
teddy.params.templateMaxCaches[String(template)] = parseInt(v);
},

// mutator method to set a whitelist of templates to cache, e.g. { "myTemplate.html": maxCaches} where maxCaches is an integer
setCacheWhitelist: function(o) {
var i;
teddy.params.cacheWhitelist = o;
for (i in o) {
teddy.setMaxCaches(i, o[i]);
}
},

// mutator method to set a blacklist of templates not to cache as an array
setCacheBlacklist: function(a) {
teddy.params.cacheBlacklist = a;
},

// turn on or off the setting to compile templates at every render
compileAtEveryRender: function(v) {
teddy.params.compileAtEveryRender = Boolean(v);
Expand Down Expand Up @@ -177,7 +205,7 @@

// compiles a template (removes {! comments !} and unnecessary whitespace)
compile: function(template) {
var name = template, oldTemplate, comments, l, i;
var name = template, oldTemplate, comments, l, i, register = false;

// it's assumed that the argument is already a template string if we're not server-side
if (typeof template !== 'string') {
Expand All @@ -189,6 +217,7 @@

// get contents of file if template is a file
if (template.indexOf('<') === -1 && fs) {
register = true;
try {
template = fs.readFileSync(template, 'utf8');
}
Expand All @@ -202,6 +231,7 @@
}
catch (e) {
// do nothing, attempt to render it as code
register = false;
}
}
}
Expand All @@ -226,13 +256,13 @@
}
while (oldTemplate !== template);

// append extension if not present
if (name.slice(-5) !== '.html') {
name += '.html';
if (register) {
teddy.templates[name] = template;
return template;
}
else {
return template.slice(-5) === '.html' ? template.substring(0, template.length - 5) : template;
}

teddy.templates[name] = template;
return template;
},

// invalidates cache of a given template and model combination
Expand Down Expand Up @@ -284,21 +314,10 @@
passes = 0,
renders,
render,
stringyModel,
maxPasses = teddy.params.maxPasses,
maxPassesError = 'Render aborted due to max number of passes (' + maxPasses + ') exceeded; there is a possible infinite loop in your template logic.';

if (teddy.params.cacheRenders) {
teddy.renderedTemplates[template] = teddy.renderedTemplates[template] || [];
renders = teddy.renderedTemplates[template];
l = renders.length;
for (i = 0; i < l; i++) {
render = renders[i];
if (JSON.stringify(render.model) === JSON.stringify(model)) {
return render.renderedTemplate;
}
}
}

// overload conosle logs
consoleWarnings = '';
consoleErrors = '';
Expand All @@ -314,28 +333,54 @@
}

// remove templateRoot from template name if necessary
template = replaceNonRegex(template, teddy.params.templateRoot, '');

// compile template if necessary
if (!teddy.templates[template] || teddy.params.compileAtEveryRender) {
teddy.compile(template);
if (template.slice(teddy.params.templateRoot.length) === teddy.params.templateRoot) {
template = template.replace(teddy.params.templateRoot, '');
}

// append extension if not present
if (template.slice(-5) !== '.html') {
template += '.html';
}

if (teddy.params.cacheRenders) {
// return cached template if one exists
if (teddy.params.cacheRenders && teddy.templates[template] && (!teddy.params.cacheWhitelist || teddy.params.cacheWhitelist[template]) && teddy.params.cacheBlacklist.indexOf(template) < 0) {
stringyModel = JSON.stringify(model);
teddy.renderedTemplates[template] = teddy.renderedTemplates[template] || [];
renders = teddy.renderedTemplates[template];
l = renders.length;
for (i = 0; i < l; i++) {
render = renders[i];
if (JSON.stringify(render.model) === stringyModel) {

// move to last position in the array to mark it as most recently accessed
teddy.renderedTemplates[template].push(teddy.renderedTemplates[template].splice(i, 1)[0]);
return render.renderedTemplate;
}
}
}

// compile template if necessary
if (!teddy.templates[template] || teddy.params.compileAtEveryRender) {
renderedTemplate = teddy.compile(template);
}

renderedTemplate = teddy.templates[template] || renderedTemplate;

// prepare to cache the template if caching is enabled and this template is eligible
if (teddy.params.cacheRenders && teddy.templates[template] && (!teddy.params.cacheWhitelist || teddy.params.cacheWhitelist[template]) && teddy.params.cacheBlacklist.indexOf(template) < 0) {
teddy.renderedTemplates[template] = teddy.renderedTemplates[template] || [];
l = teddy.renderedTemplates[template].length;

// remove first (oldest) item from the array if cache limit is reached
if ((teddy.params.templateMaxCaches[template] && l >= teddy.params.templateMaxCaches[template]) || (!teddy.params.templateMaxCaches[template] && l >= teddy.params.defaultCaches)) {
teddy.renderedTemplates[template].shift();
}
l = teddy.renderedTemplates[template].push({
renderedTemplate: '',
model: Object.assign({}, model)
});
}

renderedTemplate = teddy.templates[template];

if (!renderedTemplate) {
if (teddy.params.verbosity) {
teddy.console.warn('teddy.render attempted to render a template which doesn\'t exist: ' + template);
Expand Down Expand Up @@ -462,7 +507,8 @@
consoleErrors = '';
}

if (teddy.params.cacheRenders) {
// cache the template if caching is enabled and this template is eligible
if (teddy.params.cacheRenders && teddy.templates[template] && (!teddy.params.cacheWhitelist || teddy.params.cacheWhitelist[template]) && teddy.params.cacheBlacklist.indexOf(template) < 0) {
teddy.renderedTemplates[template][l - 1].renderedTemplate = renderedTemplate;
}

Expand Down Expand Up @@ -692,11 +738,11 @@

// compile included template if necessary
if (!teddy.templates[src] || teddy.params.compileAtEveryRender) {
teddy.compile(src);
incdoc = teddy.compile(src);
}

// get the template as a string
incdoc = teddy.templates[src];
incdoc = teddy.templates[src] || incdoc;
if (!incdoc) {
if (teddy.params.verbosity) {
teddy.console.warn('<include> element found which references a nonexistent template ("' + src + '"). Ignoring element.');
Expand Down Expand Up @@ -1271,7 +1317,12 @@
}

function replaceNonRegex(str, find, replace) {
return str.split(find).join(replace);
if (typeof str === 'string') {
return str.split(find).join(replace);
}
else {
teddy.console.error('teddy: replaceNonRegex passed invalid arguments.');
}
}

// expose as a CommonJS module
Expand Down
2 changes: 1 addition & 1 deletion test/includes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ if (typeof module !== 'undefined') {

describe('Includes', function() {
before(function() {
this.timeout(5000);
teddy.setTemplateRoot('test/templates');
model = makeModel();
});

Expand Down
9 changes: 7 additions & 2 deletions test/looping.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ if (typeof module !== 'undefined') {

describe('Looping', function() {
before(function() {
teddy.setVerbosity(0);
teddy.cacheRenders(true);
teddy.setTemplateRoot('test/templates');
model = makeModel();
});

Expand Down Expand Up @@ -47,6 +46,7 @@ describe('Looping', function() {
});

it('should loop through an array of 5000 elements in < 5000ms (looping/largeDataSet.html)', function(done) {
teddy.cacheRenders(true);
var start, end, time;
start = new Date().getTime();

Expand Down Expand Up @@ -122,17 +122,22 @@ describe('Looping', function() {
end = new Date().getTime();
time = end - start;

teddy.cacheRenders(false);
assert.isAtMost(time, 50);
done();
});

it('should ignore loop with invalid through attribute (looping/undefinedObjectLoop.html)', function(done) {
teddy.setVerbosity(0);
assert.equalIgnoreSpaces(teddy.render('looping/undefinedObjectLoop.html', model), '<div></div>');
teddy.setVerbosity(1);
done();
});

it('should ignore loop with no contents (looping/emptyMarkupLoop.html)', function(done) {
teddy.setVerbosity(0);
assert.equalIgnoreSpaces(teddy.render('looping/emptyMarkupLoop.html', model), '<div></div>');
teddy.setVerbosity(1);
done();
});

Expand Down
55 changes: 55 additions & 0 deletions test/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ if (typeof module !== 'undefined') {

describe('Misc', function() {
before(function() {
teddy.setTemplateRoot('test/templates');
model = makeModel();
});

Expand Down Expand Up @@ -73,4 +74,58 @@ describe('Misc', function() {
assert.equalIgnoreSpaces(teddy.render('misc/regexEscaping.html', model), '<input type=\'text\' name=\'date\' placeholder=\'DD/MM/YYYY\' id=\'date\' pattern=\'^(3[0-1]|[1-2]\\d|[1-9]|0\\d)\\/(1[0-2]|[1-9]|0\\d)\\/[1-2]\\d{3}$\'>');
done();
});

it('should trigger caching rollover given one template with 100 unique models (misc/variable.html)', function(done) {
var i;
teddy.cacheRenders(true);
teddy.setDefaultCaches(10);
for (i = 0; i < 100; i++) {
teddy.render('misc/variable.html', {something: i});
}
assert.equalIgnoreSpaces(teddy.renderedTemplates['misc/variable.html'][0].renderedTemplate, '<p>90</p>');
teddy.setDefaultCaches(1);
teddy.cacheRenders(false);
done();
});

it('should not cache a blacklisted template (misc/variable.html)', function(done) {
teddy.cacheRenders(true);
teddy.renderedTemplates = {};
teddy.setCacheBlacklist(['misc/variable.html']);
teddy.render('misc/variable.html', {something: 1});
assert.equal(teddy.renderedTemplates['misc/variable.html'], undefined);
teddy.setCacheBlacklist([]);
teddy.cacheRenders(false);
done();
});

it('should only cache whitelisted templates (misc/variable.html)', function(done) {
teddy.cacheRenders(true);
teddy.renderedTemplates = {};
teddy.setCacheWhitelist({'misc/variable.html': 1});
teddy.render('misc/plainHTML.html', {something: 1});
teddy.render('misc/variable.html', {something: 1});
assert.equal(teddy.renderedTemplates['misc/plainHTML.html'], undefined);
assert.equalIgnoreSpaces(teddy.renderedTemplates['misc/variable.html'][0].renderedTemplate, '<p>1</p>');
teddy.setCacheWhitelist({});
teddy.cacheRenders(false);
done();
});

it('should only cache the whitelisted template the specified number of times (misc/variable.html)', function(done) {
var i;
teddy.cacheRenders(true);
teddy.renderedTemplates = {};
teddy.setDefaultCaches(10);
teddy.setCacheWhitelist({'misc/variable.html': 10});
teddy.render('misc/plainHTML.html', {something: 1});
for (i = 0; i < 100; i++) {
teddy.render('misc/variable.html', {something: i});
}
assert.equal(teddy.renderedTemplates['misc/plainHTML.html'], undefined);
assert.equalIgnoreSpaces(teddy.renderedTemplates['misc/variable.html'][0].renderedTemplate, '<p>90</p>');
teddy.setCacheWhitelist({});
teddy.cacheRenders(false);
done();
});
});

0 comments on commit f688f7e

Please sign in to comment.