Skip to content

Instantly share code, notes, and snippets.

@malko
Created December 10, 2012 15:57
Show Gist options
  • Save malko/4251434 to your computer and use it in GitHub Desktop.
Save malko/4251434 to your computer and use it in GitHub Desktop.
minimalistic template system
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>jsonFilter test suite</title>
<link type="text/css" rel="stylesheet" href="qunit-1.10.0.css" media="all" />
</head>
<body>
<div id="qunit"></div>
<script type="text/javascript" src="qunit-1.10.0.js"></script>
<script type="text/javascript" src="../stepl.js"></script>
<script type="text/tpl" rel="personInfo">
<div>{{firstname}} {{lastname}} is {{age}} years old</div>
</script>
<script type="text/tpl" rel="presonInfoUnEscapedDesc">
<div>{{firstname}} {{lastname}} is {{age}} years old {{description}}</div>
</script>
<script type="text/tpl" rel="presonInfoEscapedDesc">
<div>{{firstname}} {{lastname}} is {{age}} years old {{{description}}}</div>
</script>
<script type="text/tpl" rel="children">
<ul>
{{#childs}}<li>{{firstname}} {{lastname}}</li>{{/childs}}
</ul>
</script>
<script>
function stronger(a){
return '<strong>'+a+'</strong>';
}
function firstLetter(a){
return a.substr(0,1);
}
function getInitials(a){
return firstLetter(a.firstname)+'.'+firstLetter(a.lastname)+'.';
}
function count(a){ return a.length };
var testDatas= {
firstname:'John'
,lastname:'Doe'
,age:55
,description:'<b>"it\'s a string which contains escapable chars"</b>'
,childs:[
{
firstname:'Jack'
,lastname:'Doe'
,age:20
}
,{
firstname:'Jannice'
,lastname:'Doe'
,age:16
}
]
};
test("basic strings replacement", function() {
equal(tpl('personInfo',testDatas),'<div>John Doe is 55 years old</div>',"replacing multiple double stashed values");
equal(tpl('presonInfoUnEscapedDesc',testDatas),'<div>John Doe is 55 years old <b>"it\'s a string which contains escapable chars"</b></div>',"checking escapable values with double stash");
equal(tpl('presonInfoEscapedDesc',testDatas),'<div>John Doe is 55 years old &lt;b&gt;&quot;it&#x27;s a string which contains escapable chars&quot;&lt;/b&gt;</div>',"checking escapable values with double stash");
});
test("working with list of elements and filters", function() {
equal(tpl('children',testDatas),'<ul>\n\
<li>Jack Doe</li><li>Jannice Doe</li>\n\
</ul>','each elements');
equal(tpl.renderString('<div>{{childs|count}} {{childs.length}}</div>',testDatas),'<div>2 2</div>','getting count by .length and filter');
equal(tpl.renderString('<div>{{childs|count}}{{#childs}} {{.|getInitials}}{{/childs}}</div>',testDatas),'<div>2 J.D. J.D.</div>','getting dotted property + filtering');
});
test('Working with conditionals replacement',function(){
tpl.registerFilter('isJack',function(a){return (a==='Jack')?a:null;});
equal(tpl.renderString('{{#childs}}{{firstname|isJack?}} {{firstname}} {{lastname|firstLetter}}.{{?firstname|isJack}}{{/childs}}',testDatas),' Jack D.','testing filtered prop');
equal(tpl.renderString('{{#childs}}{{firstname|isJack!?}} {{firstname}} {{lastname|firstLetter}}.{{?firstname|isJack}}{{/childs}}',testDatas),' Jannice D.','testing negative filtered prop');
});
test('accessing ancestor properties in each child elements',function(){
equal(
tpl.renderString('{{#childs}}<div>{{firstname}} is child of {{../firstname}} {{../lastname}}</div>{{/childs}}',testDatas)
,'<div>Jack is child of John Doe</div><div>Jannice is child of John Doe</div>'
,'accessing ancestore property by ../'
);
equal(
tpl.renderString('{{#childs}}<div>{{firstname}} is child of {{/firstname}} {{/lastname}}</div>{{/childs}}',testDatas)
,'<div>Jack is child of John Doe</div><div>Jannice is child of John Doe</div>'
,'accessing ancestore property by /'
);
});
</script>
</body>
</html>
(function(exports){
/*jshint expr:true*/
"use strict";
var tplType = 'text/tpl'
, tplStrings = {}
, registeredFilters = {}
, escapedchars={ "&": "&amp;" ,"<": "&lt;" ,">": "&gt;" ,'"': "&quot;" ,"'": "&#x27;" ,"`": "&#x60;" }
, G = (typeof global !== 'undefined') ? global : (new Function('return this;'))()
;
function escaped(str){
return str.replace(/[<>"'`]|&(?!amp;)/g,function(m){ return escapedchars[m];});
}
function registerString(tplName,tplStr){
tplStrings[tplName] = tplStr.replace(/^\s+|\s+$/g,'');
}
function registerScriptTag(scriptTag,name){
registerString(name || scriptTag.getAttribute('rel'),scriptTag.innerHTML);
scriptTag.parentNode.removeChild(scriptTag);
}
function registerFilter(filterName,filterMethod){
registeredFilters[filterName] = filterMethod || G[filterName];
}
function preload(){
var tplScripts,i;
if( document.querySelectorAll ){
tplScripts = document.querySelectorAll('script[type="'+tplType+'"][rel]');
}else{
var tmp = document.getElementsByTagName('script');
for( i=tmp.length,tplScripts=[]; i--;){
tmp[i].getAttribute('type') === tplType && tmp[i].getAttribute('rel') && tplScripts.push(tmp[i]);
}
}
for( i = tplScripts.length;i--;){
registerScriptTag(tplScripts[i]);
}
}
function getDataKey(datas,key,context){
if( ~key.indexOf('|') ){
var filters=key.split('|'), data,i,l;
key = filters.shift();
data = getDataKey(datas,key,context);
for(i=0,l=filters.length;i<l;i++){
if( ! registeredFilters[filters[i]] ){
registerFilter(filters[i]);
}
data = registeredFilters[filters[i]](data);
}
return data;
}
if(key.match(/^\//)){
key=key.substr(1);
context='';
}
while(key.match(/^..\//)){
context = (~context.indexOf('.'))?'':context.replace(/\.[^\.]*$/,'');
key = key.replace(/^..\//,'');
}
key =key.replace(/^\.+/,'');
key = ((context||'')+(context && key?'.':'')+key).split('.');
var res = datas;
for(var i=0,l=key.length;i<l;i++){
if(! (key[i] in res) ){ return null;}
res=res[key[i]];
}
return res;
}
function render(str,datas,context){
context || (context='');
return str
.replace(/\{\{\s*#([^\{\s]+?)\}\}([\s\S]*?)\{\{\/\1\}\}/g,function(m,param,str){ // each elements #param ending /param
var res = [],data = getDataKey(datas,param,context),nContext = (context?context+'.':'')+param;
if(! data ){ return '';}
for( var i in data ){
data.hasOwnProperty(i) && res.push(render(str,datas,nContext+'.'+i));
}
return res.join('');
})
.replace(/\{\{([^\{\s]+?)(!)?\?\s*\}\}([\s\S]*?)\{\{\?\1\}\}/g,function(m,param,not,str){ // if => param? if not => param!? both ending with ?param
var data = getDataKey(datas,param,context);
var paramTrue = (!data)? false : (data instanceof Array ? data.length : true);
not && (paramTrue = !paramTrue);
return paramTrue ? render(str,datas,context) : '';
})
.replace(/\{\{\{([^\{\s]*?)\}\}\}/g,function(m,id){ // triple stash for escaped replacement
return escaped(getDataKey(datas,id,context));
})
.replace(/\{\{([^\{\s]*?)\}\}/g,function(m,id){ return getDataKey(datas,id,context);}) // double stash for normal replacement
;
}
function tpl(name,datas){
if( tplStrings[name] === undefined){
preload();
if( tplStrings[name] === undefined){
return false;
}
}
return render(tplStrings[name],datas);
}
tpl.registerScriptTag = registerScriptTag;
tpl.registerString = registerString;
tpl.registerFilter = function(name,filter){registerFilter(name,filter); return this;};
tpl.preload= preload;
tpl.renderString=render;
exports.tpl = tpl;
})((typeof exports === 'object') ? exports : this);

given this datas:

{
  company:'My company'
  ,persons:[
    {firstname:'John',lastname:'Doe',isBoss:false}
    {firstname:'Manuel',lastname:'Ramirez',isBoss:true}
  ]

##each persons

<ul>
{{#persons}}
<li> {{firstname}} {{lastname}} </li>
{{/persons}}
</ul>

##if

<ul>
{{#persons}}
<li> {{firstname}} {{lastname}} {{isBoss?}}is a boss{{?isBoss}}</li>
{{/persons}}
</ul>

##if not

<ul>
{{#persons}}
<li> {{firstname}} {{lastname}} {{isBoss!?}}is not a boss{{?isBoss}}</li>
{{/persons}}
</ul>
@malko
Copy link
Author

malko commented Jul 26, 2013

Moved to its own repository: https://github.com/malko/stpl

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment