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\/:;&#]*)["\'])/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, 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, 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);