RML.js
RML.js
Generate markup with JavaScript
View the updated README on the github repo.
The code
var RML = (function() {
//vars...
var SLICE = Array.prototype.slice,
TOSTRING = Object.prototype.toString,
ARRAY = '[object Array]',
FUNCTION = '[object Function]',
OBJECT = '[object Object]',
//factor out some redundant code, function is passed the string array,
//pre-join, a string arg, and the tag type (0,1,2)
factory = function(tstr, arg, tt, dl) {
//the original tag is in the tstr
var t = tstr[1];
//handle arg = array
if(TOSTRING.call(arg) === ARRAY) {
arg = arg.join(dl);
}
switch(tt) {
case 1:
case true: //remain backwards compatible with older RML code
//self closing tag
tstr.push(' ', arg, '/>');
break;
case 2:
//single tag form
tstr.push(' ', arg, '>');
break;
default:
//undefined tt = default type with closing tag
tstr.push('>', arg, '</', t, '>');
}
return tstr.join('');
},
//assemble the string arg from an object and return
handleObj = function(tstr, arg, tt, that, dl) {
var content = false, t = tstr[1], prop;
//override bool for content if present
if (that.has(arg, 'content')) {content = true;}
//assemble the properties section of the tag
for (prop in arg) {
if(that.has(arg, prop) && !that.filter[prop]) {
tstr.push(that.rsub(prop, arg[prop]));
}
}
//deal with possible content based on tag type
switch(tt) {
case 1:
case true: //backwards compatible
//self closing, no content
tstr.push('/>');
break;
case 2:
//open tag form, no content
tstr.push('>');
break;
default:
//default tag type with closing tag, may have content
tstr.push('>');
if(content) {
switch(TOSTRING.call(arg.content)) {
case FUNCTION:
tstr.push(arg.content());
break;
case ARRAY:
tstr.push(arg.content.join(dl));
break;
default:
tstr.push(arg.content);
}
}
tstr.push('</', t, '>');
}
return tstr.join('');
};
return {
//shortcut methods to append to RML
tags: {
'a': 0,'b': 0,'br': 1,
'canvas': 0,'code': 0,'div': 0,
'dl': 0, 'dd': 0, 'dt': 0,
'em': 0,'form': 0,'h1': 0,
'h2': 0,'h3': 0,'h4': 0,
'hr': 1,'i': 0,'img': 0,
'input': 1,'li': 0,'link': 1,
'ol': 0,'p': 0,'pre': 0,
'script': 0,'select': 0, 'strong': 0,
'span': 0,'table': 0,'tbody': 0,
'td': 0,'textarea': 0,'th': 0,
'thead': 0,'tr': 0,'ul': 0
},
//items which should NOT appear in the
//attributes of a tag. Items set to true
//will be excluded
filter: {
'content': true
},
//absract out the string replacement bit.
rsub: function(k,v) {
// AFAIK there is no atribute with an underscore,
// we'll use this to replace '_class' with 'class'
k = k.replace(/_/, '');
//can accomodate html5 attributes like the video tag 'controls'
var str = v ? ' %1="%2"' : ' %1';
return v ? str.replace('%1', k).replace('%2', v) :
str.replace('%1', k);
},
//convenience method for hasOwnProperty
//@param obj is the object in question
//@param key is the key to look for
has: function(obj, key) {
return obj && obj.hasOwnProperty && obj.hasOwnProperty(key);
},
//method to abstract over the typical operation
//of joining or (worse) concatonating sibling elements
siblings: function() {
//could have any number of arguments
var args = SLICE.call(arguments, 0);
return args.join('');
},
//tag names are methods appended to the RML object
//which will return a markup string
//@param arg will contain the attributes and content of the
//desired tag, step through it and modify the tstr to match
//@param t is the actual tag name, this allows for custom tags too
//@param tt is an int representing tag type:
//falsy values (0 for ex.) will create a tag with a closing tag, <a></a>
//1 (or true) for a self closing tag, <br /> **these are not checked for content
//2 for the open tag form <foo> **also not checked for content
//dl is an optional arg which can provide a delimiter for content arrays
tag: function(t, arg, tt, dl) {
arg = arg || '';
dl = dl || ''; //default no spaces between array items
var tstr = ['<', t];
//handle the arg type, a string or an object, then
//hand off to appropriate function
return TOSTRING.call(arg) !== OBJECT ? factory(tstr, arg, tt, dl) :
handleObj(tstr, arg, tt, this, dl);
}
};
}());
//dynamically append convenience methods using tags object
(function() {
var has = RML.has,
tags = RML.tags,
prop;
for(prop in tags) {
if (has(tags, prop)) {
RML[prop] = (function(t) {
return function(arg) {
return this.tag(t, arg, this.tags[t]);
};
}(prop));
}
}
}());
//vars...
var SLICE = Array.prototype.slice,
TOSTRING = Object.prototype.toString,
ARRAY = '[object Array]',
FUNCTION = '[object Function]',
OBJECT = '[object Object]',
//factor out some redundant code, function is passed the string array,
//pre-join, a string arg, and the tag type (0,1,2)
factory = function(tstr, arg, tt, dl) {
//the original tag is in the tstr
var t = tstr[1];
//handle arg = array
if(TOSTRING.call(arg) === ARRAY) {
arg = arg.join(dl);
}
switch(tt) {
case 1:
case true: //remain backwards compatible with older RML code
//self closing tag
tstr.push(' ', arg, '/>');
break;
case 2:
//single tag form
tstr.push(' ', arg, '>');
break;
default:
//undefined tt = default type with closing tag
tstr.push('>', arg, '</', t, '>');
}
return tstr.join('');
},
//assemble the string arg from an object and return
handleObj = function(tstr, arg, tt, that, dl) {
var content = false, t = tstr[1], prop;
//override bool for content if present
if (that.has(arg, 'content')) {content = true;}
//assemble the properties section of the tag
for (prop in arg) {
if(that.has(arg, prop) && !that.filter[prop]) {
tstr.push(that.rsub(prop, arg[prop]));
}
}
//deal with possible content based on tag type
switch(tt) {
case 1:
case true: //backwards compatible
//self closing, no content
tstr.push('/>');
break;
case 2:
//open tag form, no content
tstr.push('>');
break;
default:
//default tag type with closing tag, may have content
tstr.push('>');
if(content) {
switch(TOSTRING.call(arg.content)) {
case FUNCTION:
tstr.push(arg.content());
break;
case ARRAY:
tstr.push(arg.content.join(dl));
break;
default:
tstr.push(arg.content);
}
}
tstr.push('</', t, '>');
}
return tstr.join('');
};
return {
//shortcut methods to append to RML
tags: {
'a': 0,'b': 0,'br': 1,
'canvas': 0,'code': 0,'div': 0,
'dl': 0, 'dd': 0, 'dt': 0,
'em': 0,'form': 0,'h1': 0,
'h2': 0,'h3': 0,'h4': 0,
'hr': 1,'i': 0,'img': 0,
'input': 1,'li': 0,'link': 1,
'ol': 0,'p': 0,'pre': 0,
'script': 0,'select': 0, 'strong': 0,
'span': 0,'table': 0,'tbody': 0,
'td': 0,'textarea': 0,'th': 0,
'thead': 0,'tr': 0,'ul': 0
},
//items which should NOT appear in the
//attributes of a tag. Items set to true
//will be excluded
filter: {
'content': true
},
//absract out the string replacement bit.
rsub: function(k,v) {
// AFAIK there is no atribute with an underscore,
// we'll use this to replace '_class' with 'class'
k = k.replace(/_/, '');
//can accomodate html5 attributes like the video tag 'controls'
var str = v ? ' %1="%2"' : ' %1';
return v ? str.replace('%1', k).replace('%2', v) :
str.replace('%1', k);
},
//convenience method for hasOwnProperty
//@param obj is the object in question
//@param key is the key to look for
has: function(obj, key) {
return obj && obj.hasOwnProperty && obj.hasOwnProperty(key);
},
//method to abstract over the typical operation
//of joining or (worse) concatonating sibling elements
siblings: function() {
//could have any number of arguments
var args = SLICE.call(arguments, 0);
return args.join('');
},
//tag names are methods appended to the RML object
//which will return a markup string
//@param arg will contain the attributes and content of the
//desired tag, step through it and modify the tstr to match
//@param t is the actual tag name, this allows for custom tags too
//@param tt is an int representing tag type:
//falsy values (0 for ex.) will create a tag with a closing tag, <a></a>
//1 (or true) for a self closing tag, <br /> **these are not checked for content
//2 for the open tag form <foo> **also not checked for content
//dl is an optional arg which can provide a delimiter for content arrays
tag: function(t, arg, tt, dl) {
arg = arg || '';
dl = dl || ''; //default no spaces between array items
var tstr = ['<', t];
//handle the arg type, a string or an object, then
//hand off to appropriate function
return TOSTRING.call(arg) !== OBJECT ? factory(tstr, arg, tt, dl) :
handleObj(tstr, arg, tt, this, dl);
}
};
}());
//dynamically append convenience methods using tags object
(function() {
var has = RML.has,
tags = RML.tags,
prop;
for(prop in tags) {
if (has(tags, prop)) {
RML[prop] = (function(t) {
return function(arg) {
return this.tag(t, arg, this.tags[t]);
};
}(prop));
}
}
}());
You could drastically reduce the size of your source if you used some dynamic programming to generate all of your convenience methods.
for(var i = 0, a = ['a', 'div', ...], l=a.length; i < l; ++i){
RML[a[i]] = function(arg) { return RML.tag(a[i], arg); }
}
Absolutely. I’ve thought about this a bit. Once I have a somewhat ‘complete’ set of tags in I intend to set up a method which abstracts out all of that repeated code in the convenience methods. You hit the nail on the head with the array of tag names assigned to the ‘a’ variable. The question becomes then should this thing that holds the tag names be an array (probably) or an object. The only reason I can see (atm) for it to be an object is to provide an easily configured/overridden interface for users akin to what the filter is doing. I like yours here though, mine had an array outside of the for loop, those big for loops are sheik…
The one problem this solution has, and mine as well at the moment, is that the methods will be appended correctly to the object but the return statement’s function will have undefined tag names.
Solved!