666 lines
No EOL
21 KiB
JavaScript
666 lines
No EOL
21 KiB
JavaScript
var Plates = (typeof module !== 'undefined' && 'id' in module && typeof exports !== 'undefined') ? exports : {};
|
|
|
|
!function(exports, env, undefined) {
|
|
"use strict";
|
|
|
|
//
|
|
// Cache variables to increase lookup speed.
|
|
//
|
|
var _toString = Object.prototype.toString;
|
|
|
|
//
|
|
// Polyfill the Array#indexOf method for cross browser compatibility.
|
|
//
|
|
[].indexOf || (Array.prototype.indexOf = function indexOf(a, b ,c){
|
|
for (
|
|
c = this.length , b = (c+ ~~b) % c;
|
|
b < c && (!(b in this) || this[b] !==a );
|
|
b++
|
|
);
|
|
|
|
return b^c ? b : -1;
|
|
});
|
|
|
|
//
|
|
// Polyfill Array.isArray for cross browser compatibility.
|
|
//
|
|
Array.isArray || (Array.isArray = function isArray(a) {
|
|
return _toString.call(a) === '[object Array]';
|
|
});
|
|
|
|
//
|
|
// ### function fetch(data, mapping, value, key)
|
|
// #### @data {Object} the data that we need to fetch a value from
|
|
// #### @mapping {Object} The iterated mapping step
|
|
// #### @tagbody {String} the tagbody we operated against
|
|
// #### @key {String} optional key if the mapping doesn't have a dataKey
|
|
// Fetches the correct piece of data
|
|
//
|
|
function fetch(data, mapping, value, tagbody, key) {
|
|
key = mapping.dataKey || key;
|
|
|
|
//
|
|
// Check if we have data manipulation or filtering function.
|
|
//
|
|
if (mapping.dataKey && typeof mapping.dataKey === 'function') {
|
|
return mapping.dataKey(data, value || '', tagbody || '', key);
|
|
}
|
|
|
|
//
|
|
// See if we are using dot notation style
|
|
//
|
|
if (!~key.indexOf('.')) return data[key];
|
|
|
|
var result = key
|
|
, structure = data;
|
|
|
|
for (var paths = key.split('.'), i = 0, length = paths.length; i < length && structure; i++) {
|
|
result = structure[+paths[i] || paths[i]];
|
|
structure = result;
|
|
}
|
|
|
|
return result !== undefined ? result : data[key];
|
|
}
|
|
|
|
//
|
|
// compileMappings
|
|
//
|
|
// sort the mappings so that mappings for the same attribute and value go consecutive
|
|
// and inside those, those that change attributes appear first.
|
|
//
|
|
function compileMappings(oldMappings) {
|
|
var mappings = oldMappings.slice(0);
|
|
|
|
mappings.sort(function(map1, map2) {
|
|
if (!map1.attribute) return 1;
|
|
if (!map2.attribute) return -1;
|
|
|
|
if (map1.attribute !== map2.attribute) {
|
|
return map1.attribute < map2.attribute ? -1 : 1;
|
|
}
|
|
if (map1.value !== map2.value) {
|
|
return map1.value < map2.value ? -1 : 1;
|
|
}
|
|
if (! ('replace' in map1) && ! ('replace' in map2)) {
|
|
throw new Error('Conflicting mappings for attribute ' + map1.attribute + ' and value ' + map1.value);
|
|
}
|
|
if (map1.replace) {
|
|
return 1;
|
|
}
|
|
return -1;
|
|
});
|
|
|
|
return mappings;
|
|
}
|
|
|
|
//
|
|
// Matches a closing tag to a open tag
|
|
//
|
|
function matchClosing(input, tagname, html) {
|
|
var closeTag = '</' + tagname + '>',
|
|
openTag = new RegExp('< *' + tagname + '( *|>)', 'g'),
|
|
closeCount = 0,
|
|
openCount = -1,
|
|
from, to, chunk
|
|
;
|
|
|
|
from = html.search(input);
|
|
to = from;
|
|
|
|
while(to > -1 && closeCount !== openCount) {
|
|
to = html.indexOf(closeTag, to);
|
|
if (to > -1) {
|
|
to += tagname.length + 3;
|
|
closeCount ++;
|
|
chunk = html.slice(from, to);
|
|
openCount = chunk.match(openTag).length;
|
|
}
|
|
}
|
|
if (to === -1) {
|
|
throw new Error('Unmatched tag ' + tagname + ' in ' + html)
|
|
}
|
|
|
|
return chunk;
|
|
}
|
|
|
|
var Merge = function Merge() {};
|
|
Merge.prototype = {
|
|
nest: [],
|
|
|
|
tag: new RegExp([
|
|
'<',
|
|
'(/?)', // 2 - is closing
|
|
'([-:\\w]+)', // 3 - name
|
|
'((?:[-\\w]+(?:', '=',
|
|
'(?:\\w+|["|\'](?:.*)["|\']))?)*)', // 4 - attributes
|
|
'(/?)', // 5 - is self-closing
|
|
'>'
|
|
].join('\\s*')),
|
|
|
|
//
|
|
// HTML attribute parser.
|
|
//
|
|
attr: /([\-\w]*)\s*=\s*(?:(["\'])([\-\.\w\s\/:;&#]*)\2)/gi,
|
|
|
|
//
|
|
// In HTML5 it's allowed to have to use self closing tags without closing
|
|
// separators. So we need to detect these elements based on the tag name.
|
|
//
|
|
selfClosing: /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/,
|
|
|
|
//
|
|
// ### function hasClass(str, className)
|
|
// #### @str {String} the class attribute
|
|
// #### @className {String} the className that the classAttribute should contain
|
|
//
|
|
// Helper function for detecting if a class attribute contains the className
|
|
//
|
|
hasClass: function hasClass(str, className) {
|
|
return ~str.split(' ').indexOf(className);
|
|
},
|
|
|
|
//
|
|
// ### function iterate(html, value, components, tagname, key)
|
|
// #### @html {String} peice of HTML
|
|
// #### @value {Mixed} iterateable object with data
|
|
// #### @components {Array} result of the this.tag regexp execution
|
|
// #### @tagname {String} the name of the tag that we iterate on
|
|
// #### @key {String} the key of the data that we need to extract from the value
|
|
// #### @map {Object} attribute mappings
|
|
//
|
|
// Iterate over over the supplied HTML.
|
|
//
|
|
iterate: function iterate(html, value, components, tagname, key, map) {
|
|
var output = '',
|
|
segment = matchClosing(components.input, tagname, html),
|
|
data = {};
|
|
|
|
// Is it an array?
|
|
if (Array.isArray(value)) {
|
|
// Yes: set the output to the result of iterating through the array
|
|
for (var i = 0, l = value.length; i < l; i++) {
|
|
// If there is a key, then we have a simple object and
|
|
// must construct a simple object to use as the data
|
|
if (key) {
|
|
data[key] = value[i];
|
|
} else {
|
|
data = value[i];
|
|
}
|
|
|
|
output += this.bind(segment, data, map);
|
|
}
|
|
|
|
return output;
|
|
} else if (typeof value === 'object') {
|
|
// We need to refine the selection now that we know we're dealing with a
|
|
// nested object
|
|
segment = segment.slice(components.input.length, -(tagname.length + 3));
|
|
return output += this.bind(segment, value, map);
|
|
}
|
|
|
|
return value;
|
|
},
|
|
|
|
//
|
|
// ### function bind(html, data, map)
|
|
// #### @html {String} the template that we need to modify
|
|
// #### @data {Object} data for the template
|
|
// #### @map {Mapper} instructions for the data placement in the template
|
|
// Process the actual template
|
|
//
|
|
bind: function bind(html, data, map) {
|
|
if (Array.isArray(data)) {
|
|
var output = '';
|
|
|
|
for (var i = 0, l = data.length; i<l; i++) {
|
|
output += this.bind(html, data[i], map);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
html = (html || '').toString();
|
|
data = data || {};
|
|
|
|
var that = this;
|
|
|
|
var openers = 0,
|
|
remove = 0,
|
|
components,
|
|
attributes,
|
|
mappings = map && compileMappings(map.mappings),
|
|
intag = false,
|
|
tagname = '',
|
|
isClosing = false,
|
|
isSelfClosing = false,
|
|
selfClosing = false,
|
|
matchmode = false,
|
|
createAttribute = map && map.conf && map.conf.create,
|
|
closing,
|
|
tagbody;
|
|
|
|
var c,
|
|
buffer = '',
|
|
left;
|
|
|
|
for (var i = 0, l = html.length; i < l; i++) {
|
|
c = html.charAt(i);
|
|
|
|
//
|
|
// Figure out which part of the HTML we are currently processing. And if
|
|
// we have queued up enough HTML to process it's data.
|
|
//
|
|
if (c === '!' && intag && !matchmode) {
|
|
intag = false;
|
|
buffer += html.slice(left, i + 1);
|
|
} else if (c === '<' && !intag) {
|
|
closing = true;
|
|
intag = true;
|
|
left = i;
|
|
} else if (c === '>' && intag) {
|
|
intag = false;
|
|
tagbody = html.slice(left, i + 1);
|
|
components = this.tag.exec(tagbody);
|
|
|
|
if(!components) {
|
|
intag = true;
|
|
continue;
|
|
}
|
|
|
|
isClosing = components[1];
|
|
tagname = components[2];
|
|
attributes = components[3];
|
|
selfClosing = components[4];
|
|
isSelfClosing = this.selfClosing.test(tagname);
|
|
|
|
if (matchmode) {
|
|
//
|
|
// and its a closing.
|
|
//
|
|
if (!!isClosing) {
|
|
if (openers <= 0) {
|
|
matchmode = false;
|
|
} else {
|
|
--openers;
|
|
}
|
|
} else if (!isSelfClosing) {
|
|
//
|
|
// and its not a self-closing tag
|
|
//
|
|
++openers;
|
|
}
|
|
}
|
|
|
|
if (!isClosing && !matchmode) {
|
|
//
|
|
// if there is a match in progress and
|
|
//
|
|
if (mappings && mappings.length > 0) {
|
|
for (var ii = mappings.length - 1; ii >= 0; ii--) {
|
|
var setAttribute = false
|
|
, mapping = mappings[ii]
|
|
, shouldSetAttribute = mapping.re && attributes.match(mapping.re);
|
|
|
|
//
|
|
// check if we are targetting a element only or attributes
|
|
//
|
|
if ('tag' in mapping && !this.attr.test(tagbody) && mapping.tag === tagname) {
|
|
tagbody = tagbody + fetch(data, mapping, '', tagbody);
|
|
continue;
|
|
}
|
|
|
|
tagbody = tagbody.replace(this.attr, function(str, key, q, value, a) {
|
|
var newdata;
|
|
|
|
if (shouldSetAttribute && mapping.replace !== key || remove) {
|
|
return str;
|
|
} else if (shouldSetAttribute || typeof mapping.replacePartial1 !== 'undefined') {
|
|
setAttribute = true;
|
|
|
|
//
|
|
// determine if we should use the replace argument or some value from the data object.
|
|
//
|
|
if (typeof mapping.replacePartial2 !== 'undefined') {
|
|
newdata = value.replace(mapping.replacePartial1, mapping.replacePartial2);
|
|
} else if (typeof mapping.replacePartial1 !== 'undefined' && mapping.dataKey) {
|
|
newdata = value.replace(mapping.replacePartial1, fetch(data, mapping, value, tagbody, key));
|
|
} else {
|
|
newdata = fetch(data, mapping, value, tagbody, key);
|
|
}
|
|
|
|
return key + '="' + (newdata || '') + '"';
|
|
} else if (!mapping.replace && mapping.attribute === key) {
|
|
if (
|
|
mapping.value === value ||
|
|
that.hasClass(value, mapping.value ||
|
|
mappings.conf.where === key) ||
|
|
(_toString.call(mapping.value) === '[object RegExp]' &&
|
|
mapping.value.exec(value) !== null)
|
|
) {
|
|
if (mapping.remove) {
|
|
//
|
|
// only increase the remove counter if it's not a self
|
|
// closing element. As matchmode is suffectient to
|
|
// remove tose
|
|
//
|
|
if (!isSelfClosing) remove++;
|
|
matchmode = true;
|
|
} else if (mapping.plates) {
|
|
var partial = that.bind(
|
|
mapping.plates
|
|
, typeof mapping.data === 'string' ? fetch(data, { dataKey: mapping.data }) : mapping.data || data
|
|
, mapping.mapper
|
|
);
|
|
|
|
buffer += tagbody + that.iterate(html, partial, components, tagname, undefined, map);
|
|
matchmode = true;
|
|
} else {
|
|
var v = newdata = fetch(data, mapping, value, tagbody, key);
|
|
newdata = tagbody + newdata;
|
|
|
|
if (Array.isArray(v)) {
|
|
newdata = that.iterate(html, v, components, tagname, value, map);
|
|
// If the item is an array, then we need to tell
|
|
// Plates that we're dealing with nests
|
|
that.nest.push(tagname);
|
|
} else if (typeof v === 'object') {
|
|
newdata = tagbody + that.iterate(html, v, components, tagname, value, map);
|
|
}
|
|
|
|
buffer += newdata || '';
|
|
matchmode = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return str;
|
|
});
|
|
|
|
//
|
|
// Do we need to create the attributes if they don't exist.
|
|
//
|
|
if (createAttribute && shouldSetAttribute && !setAttribute) {
|
|
var spliced = selfClosing ? 2 : 1
|
|
, close = selfClosing ? '/>': '>'
|
|
, left = tagbody.substr(0, tagbody.length - spliced);
|
|
|
|
if (left[left.length - 1] === ' ') {
|
|
left = left.substr(0, left.length - 1);
|
|
|
|
if (selfClosing) {
|
|
close = ' ' + close;
|
|
}
|
|
}
|
|
|
|
tagbody = [
|
|
left,
|
|
' ',
|
|
mapping.replace,
|
|
'="',
|
|
fetch(data, mapping),
|
|
'"',
|
|
close
|
|
].join('');
|
|
}
|
|
}
|
|
} else {
|
|
//
|
|
// if there is no map, we are just looking to match
|
|
// the specified id to a data key in the data object.
|
|
//
|
|
tagbody.replace(this.attr, function (attr, key, q, value, idx) {
|
|
if (key === map && map.conf.where || 'id' && data[value]) {
|
|
var v = data[value],
|
|
nest = Array.isArray(v),
|
|
output = nest || typeof v === 'object'
|
|
? that.iterate(html.substr(left), v, components, tagname, value, map)
|
|
: v;
|
|
|
|
// If the item is an array, then we need to tell
|
|
// Plates that we're dealing with nests
|
|
if (nest) { that.nest.push(tagname); }
|
|
|
|
buffer += nest ? output : tagbody + output;
|
|
matchmode = true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
//
|
|
// if there is currently no match in progress
|
|
// just write the tagbody to the buffer.
|
|
//
|
|
if (!matchmode && that.nest.length === 0) {
|
|
if (!remove) buffer += tagbody;
|
|
|
|
if (remove && !!isClosing) --remove;
|
|
} else if (!matchmode && that.nest.length) {
|
|
this.nest.pop();
|
|
}
|
|
} else if (!intag && !matchmode) {
|
|
//
|
|
// currently not inside a tag and there is no
|
|
// match in progress, we can write the char to
|
|
// the buffer.
|
|
//
|
|
if (!remove) buffer += c;
|
|
}
|
|
}
|
|
return buffer;
|
|
}
|
|
};
|
|
|
|
//
|
|
// ### function Mapper(conf)
|
|
// #### @conf {Object} configuration object
|
|
// Constructor function for the Mapper instance that is responsible for
|
|
// providing the mapping for the data structure
|
|
//
|
|
function Mapper(conf) {
|
|
if (!(this instanceof Mapper)) { return new Mapper(conf); }
|
|
|
|
this.mappings = [];
|
|
this.conf = conf || {};
|
|
}
|
|
|
|
//
|
|
// ### function last(newitem)
|
|
// #### @newitem {Boolean} do we need to add a new item to the mapping
|
|
// Helper function for adding new attribute maps to a Mapper instance
|
|
//
|
|
function last(newitem) {
|
|
if (newitem) {
|
|
this.mappings.push({});
|
|
}
|
|
|
|
var m = this.mappings[this.mappings.length - 1];
|
|
|
|
if (m && m.attribute && m.value && m.dataKey && m.replace) {
|
|
m.re = new RegExp(m.attribute + '=([\'"]?)' + m.value + '\\1');
|
|
} else if (m) {
|
|
delete m.re;
|
|
}
|
|
|
|
return m;
|
|
}
|
|
|
|
//
|
|
// Create the actual chainable methods: where('class').is('foo').insert('bla')
|
|
//
|
|
Mapper.prototype = {
|
|
//
|
|
// ### function replace(val1, val2)
|
|
// #### @val1 {String|RegExp} The part of the attribute that needs to be replaced
|
|
// #### @val2 {String} The value it should be replaced with
|
|
//
|
|
replace: function replace(val1, val2) {
|
|
var l = last.call(this);
|
|
l.replacePartial1 = val1;
|
|
l.replacePartial2 = val2;
|
|
return this;
|
|
},
|
|
|
|
//
|
|
// ### function use(val)
|
|
// #### @val {String} A string that represents a key.
|
|
// Data will be inserted into the attribute that was specified in the
|
|
// `where` clause.
|
|
//
|
|
use: function use(val) {
|
|
last.call(this).dataKey = val;
|
|
return last.call(this) && this;
|
|
},
|
|
|
|
//
|
|
// ### function where(val)
|
|
// #### @val {String} an attribute that may be found in a tag
|
|
// This method will initiate a clause. Once a clause has been established
|
|
// other member methods will be chained to each other in any order.
|
|
//
|
|
where: function where(val) {
|
|
last.call(this, true).attribute = val;
|
|
return last.call(this) && this;
|
|
},
|
|
|
|
//
|
|
// ### function class(val)
|
|
// #### @val {String} a value that may be found in the `class` attribute of a tag
|
|
// the method name should be wrapped in quotes or it will throw errors in IE.
|
|
//
|
|
'class': function className(val) {
|
|
return this.where('class').is(val);
|
|
},
|
|
|
|
//
|
|
// ### function tag(val)
|
|
// #### @val {String} the name of the tag should be found
|
|
//
|
|
tag: function tag(val) {
|
|
last.call(this, true).tag = val;
|
|
return this;
|
|
},
|
|
|
|
//
|
|
// ### function is(val)
|
|
// #### @val {string} The value of the attribute that was specified in the
|
|
// `where` clause.
|
|
//
|
|
is: function is(val) {
|
|
last.call(this).value = val;
|
|
return last.call(this) && this;
|
|
},
|
|
|
|
//
|
|
// ### function has(val)
|
|
// #### @val {String|RegExp} The value of the attribute that was specified
|
|
// in the `where` clause.
|
|
//
|
|
has: function has(val) {
|
|
last.call(this).value = val;
|
|
this.replace(val);
|
|
return last.call(this) && this;
|
|
},
|
|
|
|
//
|
|
// ### function insert(val)
|
|
// #### @val {String} A string that represents a key. Data will be inserted
|
|
// in to the attribute that was specified in the `where` clause.
|
|
//
|
|
insert: function insert(val) {
|
|
var l = last.call(this);
|
|
l.replace = l.attribute;
|
|
l.dataKey = val;
|
|
return last.call(this) && this;
|
|
},
|
|
|
|
//
|
|
// ### function as(val)
|
|
// #### @val {String} A string that represents an attribute in the tag.
|
|
// If there is no attribute by that name name found, one may be created
|
|
// depending on the options that where passed in the `Plates.Map`
|
|
// constructor.
|
|
//
|
|
as: function as(val) {
|
|
last.call(this).replace = val;
|
|
return last.call(this) && this;
|
|
},
|
|
|
|
//
|
|
// ### function remove()
|
|
// This will remove the element that was specified in the `where` clause
|
|
// from the template.
|
|
//
|
|
remove: function remove() {
|
|
last.call(this).remove = true;
|
|
return last.call(this, true);
|
|
},
|
|
|
|
//
|
|
// ### function append(plates, data, map)
|
|
// #### @plates {String} Template or path/id of the template
|
|
// #### @data {Object|String} data for the appended template
|
|
// #### @map {Plates.Map} mapping for the data
|
|
//
|
|
append: function append(plates, data, map) {
|
|
var l = last.call(this);
|
|
|
|
if (data instanceof Mapper) {
|
|
map = data;
|
|
data = undefined;
|
|
}
|
|
|
|
// If the supplied plates template doesn't contain any HTML it's most
|
|
// likely that we need to import it. To improve performance we will cache
|
|
// the result of the file system.
|
|
if (!/<[^<]+?>/.test(plates) && !exports.cache[plates]) {
|
|
// figure out if we are running in Node.js or a browser
|
|
if ('document' in env && 'getElementById' in env.document) {
|
|
exports.cache[plates] = document.getElementById(plates).innerHTML;
|
|
} else {
|
|
exports.cache[plates] = require('fs').readFileSync(
|
|
require('path').join(process.cwd(), plates),
|
|
'utf8'
|
|
);
|
|
}
|
|
}
|
|
|
|
l.plates = exports.cache[plates] || plates;
|
|
l.data = data;
|
|
l.mapper = map;
|
|
|
|
return last.call(this, true);
|
|
}
|
|
};
|
|
|
|
//
|
|
// Provide helpful aliases that well help with increased compatibility as not
|
|
// all browsers allow the Mapper#class prototype (IE).
|
|
//
|
|
Mapper.prototype.className = Mapper.prototype['class'];
|
|
|
|
//
|
|
// Aliases of different methods.
|
|
//
|
|
Mapper.prototype.partial = Mapper.prototype.append;
|
|
Mapper.prototype.to = Mapper.prototype.use;
|
|
|
|
//
|
|
// Expose a simple cache object so people can clear the cached partials if
|
|
// they want to.
|
|
//
|
|
exports.cache = {};
|
|
|
|
//
|
|
// Expose the Plates#bind interface.
|
|
//
|
|
exports.bind = function bind(html, data, map) {
|
|
var merge = new Merge();
|
|
return merge.bind(html, data, map);
|
|
};
|
|
|
|
//
|
|
// Expose the Mapper.
|
|
//
|
|
exports.Map = Mapper;
|
|
}(Plates, this); |