/*!
	Parser

	Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
	MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
	Dependencies  dates.js, domutils.js, html.js, isodate,js, text.js, utilities.js, url.js
*/


var Modules = (function (modules) {


	/**
	 * constructor
	 *
	 */
	modules.Parser = function () {
		this.rootPrefix = 'h-';
		this.propertyPrefixes = ['p-', 'dt-', 'u-', 'e-'];
		this.excludeTags = ['br', 'hr'];
	};


	// create objects incase the v1 map modules don't load
	modules.maps = (modules.maps)? modules.maps : {};
	modules.rels = (modules.rels)? modules.rels : {};


	modules.Parser.prototype = {

		init: function(){
			this.rootNode = null;
			this.document = null;
			this.options = {
				'baseUrl': '',
				'filters': [],
				'textFormat': 'whitespacetrimmed',
				'dateFormat': 'auto', // html5 for testing
				'overlappingVersions': false,
				'impliedPropertiesByVersion': true,
				'parseLatLonGeo': false
			};
			this.rootID = 0;
			this.errors = [];
			this.noContentErr = 'No options.node or options.html was provided and no document object could be found.';
		},


		/**
		 * internal parse function
		 *
		 * @param  {Object} options
		 * @return {Object}
		 */
		get: function(options) {
			var out = this.formatEmpty(),
				data = [],
				rels;

			this.init();
			options = (options)? options : {};
			this.mergeOptions(options);
			this.getDOMContext( options );

			// if we do not have any context create error
			if(!this.rootNode || !this.document){
				this.errors.push(this.noContentErr);
			}else{

				// only parse h-* microformats if we need to
				// this is added to speed up parsing
				if(this.hasMicroformats(this.rootNode, options)){
					this.prepareDOM( options );

					if(this.options.filters.length > 0){
						// parse flat list of items
						var newRootNode = this.findFilterNodes(this.rootNode, this.options.filters);
						data = this.walkRoot(newRootNode);
					}else{
						// parse whole document from root
						data = this.walkRoot(this.rootNode);
					}

					out.items = data;
					// don't clear-up DOM if it was cloned
					if(modules.domUtils.canCloneDocument(this.document) === false){
						this.clearUpDom(this.rootNode);
					}
				}

				// find any rels
				if(this.findRels){
					rels = this.findRels(this.rootNode);
					out.rels = rels.rels;
					out['rel-urls'] = rels['rel-urls'];
				}

			}

			if(this.errors.length > 0){
				return this.formatError();
			}
			return out;
		},


		/**
		 * parse to get parent microformat of passed node
		 *
		 * @param  {DOM Node} node
		 * @param  {Object} options
		 * @return {Object}
		 */
		getParent: function(node, options) {
			this.init();
			options = (options)? options : {};

			if(node){
				return this.getParentTreeWalk(node, options);
			}else{
				this.errors.push(this.noContentErr);
				return this.formatError();
			}
		},


	    /**
		 * get the count of microformats
		 *
		 * @param  {DOM Node} rootNode
		 * @return {Int}
		 */
		count: function( options ) {
			var out = {},
				items,
				classItems,
				x,
				i;

			this.init();
			options = (options)? options : {};
			this.getDOMContext( options );

			// if we do not have any context create error
			if(!this.rootNode || !this.document){
				return {'errors': [this.noContentErr]};
			}else{

				items = this.findRootNodes( this.rootNode, true );
				i = items.length;
				while(i--) {
					classItems = modules.domUtils.getAttributeList(items[i], 'class');
					x = classItems.length;
					while(x--) {
						// find v2 names
						if(modules.utils.startWith( classItems[x], 'h-' )){
							this.appendCount(classItems[x], 1, out);
						}
						// find v1 names
						for(var key in modules.maps) {
							// dont double count if v1 and v2 roots are present
							if(modules.maps[key].root === classItems[x] && classItems.indexOf(key) === -1) {
								this.appendCount(key, 1, out);
							}
						}
					}
				}
				var relCount = this.countRels( this.rootNode );
				if(relCount > 0){
					out.rels = relCount;
				}

				return out;
			}
		},


		/**
		 * does a node have a class that marks it as a microformats root
		 *
		 * @param  {DOM Node} node
		 * @param  {Objecte} options
		 * @return {Boolean}
		 */
		isMicroformat: function( node, options ) {
			var classes,
				i;

			if(!node){
				return false;
			}

			// if documemt gets topmost node
			node = modules.domUtils.getTopMostNode( node );

			// look for h-* microformats
			classes = this.getUfClassNames(node);
			if(options && options.filters && modules.utils.isArray(options.filters)){
				i = options.filters.length;
				while(i--) {
					if(classes.root.indexOf(options.filters[i]) > -1){
						return true;
					}
				}
				return false;
			}else{
				return (classes.root.length > 0);
			}
		},


		/**
		 * does a node or its children have microformats
		 *
		 * @param  {DOM Node} node
		 * @param  {Objecte} options
		 * @return {Boolean}
		 */
		hasMicroformats: function( node, options ) {
			var items,
				i;

			if(!node){
				return false;
			}

			// if browser based documemt get topmost node
			node = modules.domUtils.getTopMostNode( node );

			// returns all microformat roots
			items = this.findRootNodes( node, true );
			if(options && options.filters && modules.utils.isArray(options.filters)){
				i = items.length;
				while(i--) {
					if( this.isMicroformat( items[i], options ) ){
						return true;
					}
				}
				return false;
			}else{
				return (items.length > 0);
			}
		},


		/**
		 * add a new v1 mapping object to parser
		 *
		 * @param  {Array} maps
		 */
		add: function( maps ){
			maps.forEach(function(map){
				if(map && map.root && map.name && map.properties){
				modules.maps[map.name] = JSON.parse(JSON.stringify(map));
				}
			});
		},


		/**
		 * internal parse to get parent microformats by walking up the tree
		 *
		 * @param  {DOM Node} node
		 * @param  {Object} options
		 * @param  {Int} recursive
		 * @return {Object}
		 */
		getParentTreeWalk: function (node, options, recursive) {
			options = (options)? options : {};

			// recursive calls
		    if (recursive === undefined) {
		        if (node.parentNode && node.nodeName !== 'HTML'){
		            return this.getParentTreeWalk(node.parentNode, options, true);
				}else{
		            return this.formatEmpty();
				}
		    }
		    if (node !== null && node !== undefined && node.parentNode) {
		        if (this.isMicroformat( node, options )) {
					// if we have a match return microformat
					options.node = node;
		            return this.get( options );
		        }else{
		            return this.getParentTreeWalk(node.parentNode, options, true);
		        }
		    }else{
		        return this.formatEmpty();
		    }
		},



		/**
		 * configures what are the base DOM objects for parsing
		 *
		 * @param  {Object} options
		 */
		getDOMContext: function( options ){
			var nodes = modules.domUtils.getDOMContext( options );
			this.rootNode = nodes.rootNode;
			this.document = nodes.document;
		},


		/**
		 * prepares DOM before the parse begins
		 *
		 * @param  {Object} options
		 * @return {Boolean}
		 */
		prepareDOM: function( options ){
			var baseTag,
				href;

            // use current document to define baseUrl, try/catch needed for IE10+ error
            try {
                if (!options.baseUrl && this.document && this.document.location) {
                    this.options.baseUrl = this.document.location.href;
                }
            } catch (e) {
                // there is no alt action
            }


			// find base tag to set baseUrl
			baseTag = modules.domUtils.querySelector(this.document,'base');
			if(baseTag) {
				href = modules.domUtils.getAttribute(baseTag, 'href');
				if(href){
					this.options.baseUrl = href;
				}
			}

			// get path to rootNode
			// then clone document
			// then reset the rootNode to its cloned version in a new document
			var path,
				newDocument,
				newRootNode;

			path = modules.domUtils.getNodePath(this.rootNode);
			newDocument = modules.domUtils.cloneDocument(this.document);
			newRootNode = modules.domUtils.getNodeByPath(newDocument, path);

			// check results as early IE fails
			if(newDocument && newRootNode){
				this.document = newDocument;
				this.rootNode = newRootNode;
			}

			// add includes
			if(this.addIncludes){
				this.addIncludes( this.document );
			}

			return (this.rootNode && this.document);
		},


		/**
		 * returns an empty structure with errors
		 *
		 *   @return {Object}
		 */
		formatError: function(){
			var out = this.formatEmpty();
			out.errors = this.errors;
			return out;
		},


		/**
		 * returns an empty structure
		 *
		 *   @return {Object}
		 */
		formatEmpty: function(){
			return {
			    'items': [],
			    'rels': {},
			    'rel-urls': {}
			};
		},


		// find microformats of a given type and return node structures
		findFilterNodes: function(rootNode, filters) {
			var newRootNode = modules.domUtils.createNode('div'),
				items = this.findRootNodes(rootNode, true),
				i = 0,
				x = 0,
				y = 0;

			if(items){
				i = items.length;
				while(x < i) {
					// add v1 names
					y = filters.length;
					while (y--) {
						if(this.getMapping(filters[y])){
							var v1Name = this.getMapping(filters[y]).root;
							filters.push(v1Name);
						}
					}
					// append matching nodes into newRootNode
					y = filters.length;
					while (y--) {
						if(modules.domUtils.hasAttributeValue(items[x], 'class', filters[y])){
							var clone = modules.domUtils.clone(items[x]);
							modules.domUtils.appendChild(newRootNode, clone);
							break;
						}
					}
					x++;
				}
			}

			return newRootNode;
		},


		/**
		 * appends data to output object for count
		 *
		 * @param  {string} name
		 * @param  {Int} count
		 * @param  {Object}
		 */
		appendCount: function(name, count, out){
			if(out[name]){
				out[name] = out[name] + count;
			}else{
				out[name] = count;
			}
		},


		/**
		 * is the microformats type in the filter list
		 *
		 * @param  {Object} uf
		 * @param  {Array} filters
		 * @return {Boolean}
		 */
		shouldInclude: function(uf, filters) {
			var i;

			if(modules.utils.isArray(filters) && filters.length > 0) {
				i = filters.length;
				while(i--) {
					if(uf.type[0] === filters[i]) {
						return true;
					}
				}
				return false;
			} else {
				return true;
			}
		},


		/**
		 * finds all microformat roots in a rootNode
		 *
		 * @param  {DOM Node} rootNode
		 * @param  {Boolean} includeRoot
		 * @return {Array}
		 */
		findRootNodes: function(rootNode, includeRoot) {
			var arr = null,
				out = [],
				classList = [],
				items,
				x,
				i,
				y,
				key;


			// build an array of v1 root names
			for(key in modules.maps) {
				if (modules.maps.hasOwnProperty(key)) {
					classList.push(modules.maps[key].root);
				}
			}

			// get all elements that have a class attribute
			includeRoot = (includeRoot) ? includeRoot : false;
			if(includeRoot && rootNode.parentNode) {
				arr = modules.domUtils.getNodesByAttribute(rootNode.parentNode, 'class');
			} else {
				arr = modules.domUtils.getNodesByAttribute(rootNode, 'class');
			}

			// loop elements that have a class attribute
			x = 0;
			i = arr.length;
			while(x < i) {

				items = modules.domUtils.getAttributeList(arr[x], 'class');

				// loop classes on an element
				y = items.length;
				while(y--) {
					// match v1 root names
					if(classList.indexOf(items[y]) > -1) {
						out.push(arr[x]);
						break;
					}

					// match v2 root name prefix
					if(modules.utils.startWith(items[y], 'h-')) {
						out.push(arr[x]);
						break;
					}
				}

				x++;
			}
			return out;
		},


		/**
		 * starts the tree walk to find microformats
		 *
		 * @param  {DOM Node} node
		 * @return {Array}
		 */
		walkRoot: function(node){
			var context = this,
				children = [],
				child,
				classes,
				items = [],
				out = [];

			classes = this.getUfClassNames(node);
			// if it is a root microformat node
			if(classes && classes.root.length > 0){
				items = this.walkTree(node);

				if(items.length > 0){
					out = out.concat(items);
				}
			}else{
				// check if there are children and one of the children has a root microformat
				children = modules.domUtils.getChildren( node );
				if(children && children.length > 0 && this.findRootNodes(node, true).length > -1){
					for (var i = 0; i < children.length; i++) {
						child = children[i];
						items = context.walkRoot(child);
						if(items.length > 0){
							out = out.concat(items);
						}
					}
				}
			}
			return out;
		},


		/**
		 * starts the tree walking for a single microformat
		 *
		 * @param  {DOM Node} node
		 * @return {Array}
		 */
		walkTree: function(node) {
			var classes,
				out = [],
				obj,
				itemRootID;

			// loop roots found on one element
			classes = this.getUfClassNames(node);
			if(classes && classes.root.length && classes.root.length > 0){

				this.rootID++;
				itemRootID = this.rootID;
				obj = this.createUfObject(classes.root, classes.typeVersion);

				this.walkChildren(node, obj, classes.root, itemRootID, classes);
				if(this.impliedRules){
					this.impliedRules(node, obj, classes);
				}
				out.push( this.cleanUfObject(obj) );


			}
			return out;
		},


		/**
		 * finds child properties of microformat
		 *
		 * @param  {DOM Node} node
		 * @param  {Object} out
		 * @param  {String} ufName
		 * @param  {Int} rootID
		 * @param  {Object} parentClasses
		 */
		walkChildren: function(node, out, ufName, rootID, parentClasses) {
			var context = this,
				children = [],
				rootItem,
				itemRootID,
				value,
				propertyName,
				propertyVersion,
				i,
				x,
				y,
				z,
				child;

			children = modules.domUtils.getChildren( node );

			y = 0;
			z = children.length;
			while(y < z) {
				child = children[y];

				// get microformat classes for this single element
				var classes = context.getUfClassNames(child, ufName);

				// a property which is a microformat
				if(classes.root.length > 0 && classes.properties.length > 0 && !child.addedAsRoot) {
					// create object with type, property and value
					rootItem = context.createUfObject(
						classes.root,
						classes.typeVersion,
						modules.text.parse(this.document, child, context.options.textFormat)
					);

					// add the microformat as an array of properties
					propertyName = context.removePropPrefix(classes.properties[0][0]);

					// modifies value with "implied value rule"
					if(parentClasses && parentClasses.root.length === 1 && parentClasses.properties.length === 1){
						if(context.impliedValueRule){
							out = context.impliedValueRule(out, parentClasses.properties[0][0], classes.properties[0][0], value);
						}
					}

					if(out.properties[propertyName]) {
						out.properties[propertyName].push(rootItem);
					} else {
						out.properties[propertyName] = [rootItem];
					}

					context.rootID++;
					// used to stop duplication in heavily nested structures
					child.addedAsRoot = true;


					x = 0;
					i = rootItem.type.length;
					itemRootID = context.rootID;
					while(x < i) {
						context.walkChildren(child, rootItem, rootItem.type, itemRootID, classes);
						x++;
					}
					if(this.impliedRules){
						context.impliedRules(child, rootItem, classes);
					}
					this.cleanUfObject(rootItem);

				}

				// a property which is NOT a microformat and has not been used for a given root element
				if(classes.root.length === 0 && classes.properties.length > 0) {

					x = 0;
					i = classes.properties.length;
					while(x < i) {

						value = context.getValue(child, classes.properties[x][0], out);
						propertyName = context.removePropPrefix(classes.properties[x][0]);
						propertyVersion = classes.properties[x][1];

						// modifies value with "implied value rule"
						if(parentClasses && parentClasses.root.length === 1 && parentClasses.properties.length === 1){
							if(context.impliedValueRule){
								out = context.impliedValueRule(out, parentClasses.properties[0][0], classes.properties[x][0], value);
							}
						}

						// if we have not added this value into a property with the same name already
						if(!context.hasRootID(child, rootID, propertyName)) {
							// check the root and property is the same version or if overlapping versions are allowed
							if( context.isAllowedPropertyVersion( out.typeVersion, propertyVersion ) ){
								// add the property as an array of properties
								if(out.properties[propertyName]) {
									out.properties[propertyName].push(value);
								} else {
									out.properties[propertyName] = [value];
								}
								// add rootid to node so we can track its use
								context.appendRootID(child, rootID, propertyName);
							}
						}

						x++;
					}

					context.walkChildren(child, out, ufName, rootID, classes);
				}

				// if the node has no microformat classes, see if its children have
				if(classes.root.length === 0 && classes.properties.length === 0) {
					context.walkChildren(child, out, ufName, rootID, classes);
				}

				// if the node is a child root add it to the children tree
				if(classes.root.length > 0 && classes.properties.length === 0) {

					// create object with type, property and value
					rootItem = context.createUfObject(
						classes.root,
						classes.typeVersion,
						modules.text.parse(this.document, child, context.options.textFormat)
					);

					// add the microformat as an array of properties
					if(!out.children){
						out.children =  [];
					}

					if(!context.hasRootID(child, rootID, 'child-root')) {
						out.children.push( rootItem );
						context.appendRootID(child, rootID, 'child-root');
						context.rootID++;
					}

					x = 0;
					i = rootItem.type.length;
					itemRootID = context.rootID;
					while(x < i) {
						context.walkChildren(child, rootItem, rootItem.type, itemRootID, classes);
						x++;
					}
					if(this.impliedRules){
						context.impliedRules(child, rootItem, classes);
					}
					context.cleanUfObject( rootItem );

				}



				y++;
			}

		},




		/**
		 * gets the value of a property from a node
		 *
		 * @param  {DOM Node} node
		 * @param  {String} className
		 * @param  {Object} uf
		 * @return {String || Object}
		 */
		getValue: function(node, className, uf) {
			var value = '';

			if(modules.utils.startWith(className, 'p-')) {
				value = this.getPValue(node, true);
			}

			if(modules.utils.startWith(className, 'e-')) {
				value = this.getEValue(node);
			}

			if(modules.utils.startWith(className, 'u-')) {
				value = this.getUValue(node, true);
			}

			if(modules.utils.startWith(className, 'dt-')) {
				value = this.getDTValue(node, className, uf, true);
			}
			return value;
		},


		/**
		 * gets the value of a node which contains a 'p-' property
		 *
		 * @param  {DOM Node} node
		 * @param  {Boolean} valueParse
		 * @return {String}
		 */
		getPValue: function(node, valueParse) {
			var out = '';
			if(valueParse) {
				out = this.getValueClass(node, 'p');
			}

			if(!out && valueParse) {
				out = this.getValueTitle(node);
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['data','input'], 'value');
			}

			if(node.name === 'br' || node.name === 'hr') {
				out = '';
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['img', 'area'], 'alt');
			}

			if(!out) {
				out = modules.text.parse(this.document, node, this.options.textFormat);
			}

			return(out) ? out : '';
		},


		/**
		 * gets the value of a node which contains the 'e-' property
		 *
		 * @param  {DOM Node} node
		 * @return {Object}
		 */
		getEValue: function(node) {

			var out = {value: '', html: ''};

			this.expandURLs(node, 'src', this.options.baseUrl);
			this.expandURLs(node, 'href', this.options.baseUrl);

			out.value = modules.text.parse(this.document, node, this.options.textFormat);
			out.html = modules.html.parse(node);

			return out;
		},


		/**
		 * gets the value of a node which contains the 'u-' property
		 *
		 * @param  {DOM Node} node
		 * @param  {Boolean} valueParse
		 * @return {String}
		 */
		getUValue: function(node, valueParse) {
			var out = '';
			if(valueParse) {
				out = this.getValueClass(node, 'u');
			}

			if(!out && valueParse) {
				out = this.getValueTitle(node);
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['a', 'area'], 'href');
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['img','audio','video','source'], 'src');
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['object'], 'data');
			}

			// if we have no protocol separator, turn relative url to absolute url
			if(out && out !== '' && out.indexOf('://') === -1) {
				out = modules.url.resolve(out, this.options.baseUrl);
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['data','input'], 'value');
			}

			if(!out) {
				out = modules.text.parse(this.document, node, this.options.textFormat);
			}

			return(out) ? out : '';
		},


		/**
		 * gets the value of a node which contains the 'dt-' property
		 *
		 * @param  {DOM Node} node
		 * @param  {String} className
		 * @param  {Object} uf
		 * @param  {Boolean} valueParse
		 * @return {String}
		 */
		getDTValue: function(node, className, uf, valueParse) {
			var out = '';

			if(valueParse) {
				out = this.getValueClass(node, 'dt');
			}

			if(!out && valueParse) {
				out = this.getValueTitle(node);
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['time', 'ins', 'del'], 'datetime');
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
			}

			if(!out) {
				out = modules.domUtils.getAttrValFromTagList(node, ['data', 'input'], 'value');
			}

			if(!out) {
				out = modules.text.parse(this.document, node, this.options.textFormat);
			}

			if(out) {
				if(modules.dates.isDuration(out)) {
					// just duration
					return out;
				} else if(modules.dates.isTime(out)) {
					// just time or time+timezone
					if(uf) {
						uf.times.push([className, modules.dates.parseAmPmTime(out, this.options.dateFormat)]);
					}
					return modules.dates.parseAmPmTime(out, this.options.dateFormat);
				} else {
					// returns a date - microformat profile
					if(uf) {
						uf.dates.push([className, new modules.ISODate(out).toString( this.options.dateFormat )]);
					}
					return new modules.ISODate(out).toString( this.options.dateFormat );
				}
			} else {
				return '';
			}
		},


		/**
		 * appends a new rootid to a given node
		 *
		 * @param  {DOM Node} node
		 * @param  {String} id
		 * @param  {String} propertyName
		 */
		appendRootID: function(node, id, propertyName) {
			if(this.hasRootID(node, id, propertyName) === false){
				var rootids = [];
				if(modules.domUtils.hasAttribute(node,'rootids')){
					rootids = modules.domUtils.getAttributeList(node,'rootids');
				}
				rootids.push('id' + id + '-' + propertyName);
				modules.domUtils.setAttribute(node, 'rootids', rootids.join(' '));
			}
		},


		/**
		 * does a given node already have a rootid
		 *
		 * @param  {DOM Node} node
		 * @param  {String} id
		 * @param  {String} propertyName
		 * @return {Boolean}
		 */
		hasRootID: function(node, id, propertyName) {
			var rootids = [];
			if(!modules.domUtils.hasAttribute(node,'rootids')){
				return false;
			} else {
				rootids = modules.domUtils.getAttributeList(node, 'rootids');
				return (rootids.indexOf('id' + id + '-' + propertyName) > -1);
			}
		},



		/**
		 * gets the text of any child nodes with a class value
		 *
		 * @param  {DOM Node} node
		 * @param  {String} propertyName
		 * @return {String || null}
		 */
		getValueClass: function(node, propertyType) {
			var context = this,
				children = [],
				out = [],
				child,
				x,
				i;

			children = modules.domUtils.getChildren( node );

			x = 0;
			i = children.length;
			while(x < i) {
				child = children[x];
				var value = null;
				if(modules.domUtils.hasAttributeValue(child, 'class', 'value')) {
					switch(propertyType) {
					case 'p':
						value = context.getPValue(child, false);
						break;
					case 'u':
						value = context.getUValue(child, false);
						break;
					case 'dt':
						value = context.getDTValue(child, '', null, false);
						break;
					}
					if(value) {
						out.push(modules.utils.trim(value));
					}
				}
				x++;
			}
			if(out.length > 0) {
				if(propertyType === 'p') {
					return modules.text.parseText( this.document, out.join(' '), this.options.textFormat);
				}
				if(propertyType === 'u') {
					return out.join('');
				}
				if(propertyType === 'dt') {
					return modules.dates.concatFragments(out,this.options.dateFormat).toString(this.options.dateFormat);
				}
			} else {
				return null;
			}
		},


		/**
		 * returns a single string of the 'title' attr from all
		 * the child nodes with the class 'value-title'
		 *
		 * @param  {DOM Node} node
		 * @return {String}
		 */
		getValueTitle: function(node) {
			var out = [],
				items,
				i,
				x;

			items = modules.domUtils.getNodesByAttributeValue(node, 'class', 'value-title');
			x = 0;
			i = items.length;
			while(x < i) {
				if(modules.domUtils.hasAttribute(items[x], 'title')) {
					out.push(modules.domUtils.getAttribute(items[x], 'title'));
				}
				x++;
			}
			return out.join('');
		},


	   /**
		 * finds out whether a node has h-* class v1 and v2
		 *
		 * @param  {DOM Node} node
		 * @return {Boolean}
		 */
		hasHClass: function(node){
			var classes = this.getUfClassNames(node);
			if(classes.root && classes.root.length > 0){
				return true;
			}else{
				return false;
			}
		},


		/**
		 * get both the root and property class names from a node
		 *
		 * @param  {DOM Node} node
		 * @param  {Array} ufNameArr
		 * @return {Object}
		 */
		getUfClassNames: function(node, ufNameArr) {
			var context = this,
				out = {
					'root': [],
					'properties': []
				},
				classNames,
				key,
				items,
				item,
				i,
				x,
				z,
				y,
				map,
				prop,
				propName,
				v2Name,
				impiedRel,
				ufName;

			// don't get classes from excluded list of tags
			if(modules.domUtils.hasTagName(node, this.excludeTags) === false){

				// find classes for node
				classNames = modules.domUtils.getAttribute(node, 'class');
				if(classNames) {
					items = classNames.split(' ');
					x = 0;
					i = items.length;
					while(x < i) {

						item = modules.utils.trim(items[x]);

						// test for root prefix - v2
						if(modules.utils.startWith(item, context.rootPrefix)) {
							if(out.root.indexOf(item) === -1){
								out.root.push(item);
							}
							out.typeVersion = 'v2';
						}

						// test for property prefix - v2
						z = context.propertyPrefixes.length;
						while(z--) {
							if(modules.utils.startWith(item, context.propertyPrefixes[z])) {
								out.properties.push([item,'v2']);
							}
						}

						// test for mapped root classnames v1
						for(key in modules.maps) {
							if(modules.maps.hasOwnProperty(key)) {
								// only add a root once
								if(modules.maps[key].root === item && out.root.indexOf(key) === -1) {
									// if root map has subTree set to true
									// test to see if we should create a property or root
									if(modules.maps[key].subTree) {
										out.properties.push(['p-' + modules.maps[key].root, 'v1']);
									} else {
										out.root.push(key);
										if(!out.typeVersion){
											out.typeVersion = 'v1';
										}
									}
								}
							}
						}


						// test for mapped property classnames v1
						if(ufNameArr){
							for (var a = 0; a < ufNameArr.length; a++) {
								ufName = ufNameArr[a];
								// get mapped property v1 microformat
								map = context.getMapping(ufName);
								if(map) {
									for(key in map.properties) {
										if (map.properties.hasOwnProperty(key)) {

											prop = map.properties[key];
											propName = (prop.map) ? prop.map : 'p-' + key;

											if(key === item) {
												if(prop.uf) {
													// loop all the classList make sure
													//   1. this property is a root
													//   2. that there is not already an equivalent v2 property i.e. url and u-url on the same element
													y = 0;
													while(y < i) {
														v2Name = context.getV2RootName(items[y]);
														// add new root
														if(prop.uf.indexOf(v2Name) > -1 && out.root.indexOf(v2Name) === -1) {
															out.root.push(v2Name);
															out.typeVersion = 'v1';
														}
														y++;
													}
													//only add property once
													if(out.properties.indexOf(propName) === -1) {
														out.properties.push([propName,'v1']);
													}
												} else {
													if(out.properties.indexOf(propName) === -1) {
														out.properties.push([propName,'v1']);
													}
												}
											}
										}

									}
								}
							}

						}

						x++;

					}
				}
			}


			// finds any alt rel=* mappings for a given node/microformat
			if(ufNameArr && this.findRelImpied){
				for (var b = 0; b < ufNameArr.length; b++) {
					ufName = ufNameArr[b];
					impiedRel = this.findRelImpied(node, ufName);
					if(impiedRel && out.properties.indexOf(impiedRel) === -1) {
						out.properties.push([impiedRel, 'v1']);
					}
				}
			}


			//if(out.root.length === 1 && out.properties.length === 1) {
			//	if(out.root[0].replace('h-','') === this.removePropPrefix(out.properties[0][0])) {
			//		out.typeVersion = 'v2';
			//	}
			//}

			return out;
		},


		/**
		 * given a v1 or v2 root name, return mapping object
		 *
		 * @param  {String} name
		 * @return {Object || null}
		 */
		getMapping: function(name) {
			var key;
			for(key in modules.maps) {
				if(modules.maps[key].root === name || key === name) {
					return modules.maps[key];
				}
			}
			return null;
		},


		/**
		 * given a v1 root name returns a v2 root name i.e. vcard >>> h-card
		 *
		 * @param  {String} name
		 * @return {String || null}
		 */
		getV2RootName: function(name) {
			var key;
			for(key in modules.maps) {
				if(modules.maps[key].root === name) {
					return key;
				}
			}
			return null;
		},


		/**
		 * whether a property is the right microformats version for its root type
		 *
		 * @param  {String} typeVersion
		 * @param  {String} propertyVersion
		 * @return {Boolean}
		 */
		isAllowedPropertyVersion: function(typeVersion, propertyVersion){
			if(this.options.overlappingVersions === true){
				return true;
			}else{
				return (typeVersion === propertyVersion);
			}
		},


		/**
		 * creates a blank microformats object
		 *
		 * @param  {String} name
		 * @param  {String} value
		 * @return {Object}
		 */
		createUfObject: function(names, typeVersion, value) {
			var out = {};

			// is more than just whitespace
			if(value && modules.utils.isOnlyWhiteSpace(value) === false) {
				out.value = value;
			}
			// add type i.e. ["h-card", "h-org"]
			if(modules.utils.isArray(names)) {
				out.type = names;
			} else {
				out.type = [names];
			}
			out.properties = {};
			// metadata properties for parsing
			out.typeVersion = typeVersion;
			out.times = [];
			out.dates = [];
			out.altValue = null;

			return out;
		},


		/**
		 * removes unwanted microformats property before output
		 *
		 * @param  {Object} microformat
		 */
		cleanUfObject: function( microformat ) {
			delete microformat.times;
			delete microformat.dates;
			delete microformat.typeVersion;
			delete microformat.altValue;
			return microformat;
		},



		/**
		 * removes microformat property prefixes from text
		 *
		 * @param  {String} text
		 * @return {String}
		 */
		removePropPrefix: function(text) {
			var i;

			i = this.propertyPrefixes.length;
			while(i--) {
				var prefix = this.propertyPrefixes[i];
				if(modules.utils.startWith(text, prefix)) {
					text = text.substr(prefix.length);
				}
			}
			return text;
		},


		/**
		 * expands all relative URLs to absolute ones where it can
		 *
		 * @param  {DOM Node} node
		 * @param  {String} attrName
		 * @param  {String} baseUrl
		 */
		expandURLs: function(node, attrName, baseUrl){
			var i,
				nodes,
				attr;

			nodes = modules.domUtils.getNodesByAttribute(node, attrName);
			i = nodes.length;
			while (i--) {
				try{
					// the url parser can blow up if the format is not right
					attr = modules.domUtils.getAttribute(nodes[i], attrName);
					if(attr && attr !== '' && baseUrl !== '' && attr.indexOf('://') === -1) {
						//attr = urlParser.resolve(baseUrl, attr);
						attr = modules.url.resolve(attr, baseUrl);
						modules.domUtils.setAttribute(nodes[i], attrName, attr);
					}
				}catch(err){
					// do nothing - convert only the urls we can, leave the rest as they are
				}
			}
		},



		/**
		 * merges passed and default options -single level clone of properties
		 *
		 * @param  {Object} options
		 */
		mergeOptions: function(options) {
			var key;
			for(key in options) {
				if(options.hasOwnProperty(key)) {
					this.options[key] = options[key];
				}
			}
		},


		/**
		 * removes all rootid attributes
		 *
		 * @param  {DOM Node} rootNode
		 */
		removeRootIds: function(rootNode){
			var arr,
				i;

			arr = modules.domUtils.getNodesByAttribute(rootNode, 'rootids');
			i = arr.length;
			while(i--) {
				modules.domUtils.removeAttribute(arr[i],'rootids');
			}
		},


		/**
		 * removes all changes made to the DOM
		 *
		 * @param  {DOM Node} rootNode
		 */
		clearUpDom: function(rootNode){
			if(this.removeIncludes){
				this.removeIncludes(rootNode);
			}
			this.removeRootIds(rootNode);
		}


	};


	modules.Parser.prototype.constructor = modules.Parser;

	return modules;

} (Modules || {}));