User:Vаdiм/microformat-shiv.js

From Wikivoyage
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/*
   microformat-shiv - v1.3.3
   Built: 2015-12-31 01:12 - http://microformat-shiv.com
   Copyright (c) 2015 Glenn Jones
   Licensed MIT 
*/


var Microformats; // jshint ignore:line

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define([], factory);
    } else if (typeof exports === 'object') {
        module.exports = factory();
    } else {
        root.Microformats = factory();
  }
}(this, function () {
    
    var modules = {};
    

	modules.version = '1.3.3';
	modules.livingStandard = '2015-09-25T12:26:04Z';

	/**
	 * 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;


	// check parser module is loaded
	if(modules.Parser){
	
		/**
		 * applies "implied rules" microformat output structure i.e. feed-title, name, photo, url and date 
		 *
		 * @param  {DOM Node} node
		 * @param  {Object} uf (microformat output structure)
		 * @param  {Object} parentClasses (classes structure)
		 * @param  {Boolean} impliedPropertiesByVersion
		 * @return {Object}
		 */
		 modules.Parser.prototype.impliedRules = function(node, uf, parentClasses) {
			var typeVersion = (uf.typeVersion)? uf.typeVersion: 'v2';
			
			// TEMP: override to allow v1 implied properties while spec changes
			if(this.options.impliedPropertiesByVersion === false){
				typeVersion = 'v2';
			}
			
			if(node && uf && uf.properties) {
				uf = this.impliedBackwardComp( node, uf, parentClasses );  
				if(typeVersion === 'v2'){
					uf = this.impliedhFeedTitle( uf );
					uf = this.impliedName( node, uf ); 
					uf = this.impliedPhoto( node, uf ); 	
					uf = this.impliedUrl( node, uf );
				}
				uf = this.impliedValue( node, uf, parentClasses );
				uf = this.impliedDate( uf );
				
				// TEMP: flagged while spec changes are put forward
				if(this.options.parseLatLonGeo === true){
					uf = this.impliedGeo( uf );
				}  
			}

			return uf;
		};
		
		
		/**
		 * apply implied name rule
		 *
		 * @param  {DOM Node} node
		 * @param  {Object} uf
		 * @return {Object}
		 */		
		modules.Parser.prototype.impliedName = function(node, uf) {
			// implied name rule
			/*
				img.h-x[alt]										<img class="h-card" src="glenn.htm" alt="Glenn Jones"></a>
				area.h-x[alt] 										<area class="h-card" href="glenn.htm" alt="Glenn Jones"></area>
				abbr.h-x[title]										<abbr class="h-card" title="Glenn Jones"GJ</abbr>

				.h-x>img:only-child[alt]:not[.h-*]					<div class="h-card"><a src="glenn.htm" alt="Glenn Jones"></a></div>
				.h-x>area:only-child[alt]:not[.h-*] 				<div class="h-card"><area href="glenn.htm" alt="Glenn Jones"></area></div>
				.h-x>abbr:only-child[title] 						<div class="h-card"><abbr title="Glenn Jones">GJ</abbr></div>

				.h-x>:only-child>img:only-child[alt]:not[.h-*] 		<div class="h-card"><span><img src="jane.html" alt="Jane Doe"/></span></div>
				.h-x>:only-child>area:only-child[alt]:not[.h-*] 	<div class="h-card"><span><area href="jane.html" alt="Jane Doe"></area></span></div>
				.h-x>:only-child>abbr:only-child[title]				<div class="h-card"><span><abbr title="Jane Doe">JD</abbr></span></div>
			*/
			var name,
				value;
					
			if(!uf.properties.name) {
				value = this.getImpliedProperty(node, ['img', 'area', 'abbr'], this.getNameAttr);
				var textFormat = this.options.textFormat;
				// if no value for tags/properties use text
				if(!value) {
					name = [modules.text.parse(this.document, node, textFormat)];
				}else{
					name = [modules.text.parseText(this.document, value, textFormat)];
				}
				if(name && name[0] !== ''){
					uf.properties.name = name;
				}
			}
			
			return uf;
		};
		
		
		/**
		 * apply implied photo rule
		 *
		 * @param  {DOM Node} node
		 * @param  {Object} uf
		 * @return {Object}
		 */		
		modules.Parser.prototype.impliedPhoto = function(node, uf) {
			// implied photo rule
			/*
				img.h-x[src] 												<img class="h-card" alt="Jane Doe" src="jane.jpeg"/>
				object.h-x[data] 											<object class="h-card" data="jane.jpeg"/>Jane Doe</object>
				.h-x>img[src]:only-of-type:not[.h-*]						<div class="h-card"><img alt="Jane Doe" src="jane.jpeg"/></div> 
				.h-x>object[data]:only-of-type:not[.h-*] 					<div class="h-card"><object data="jane.jpeg"/>Jane Doe</object></div> 
				.h-x>:only-child>img[src]:only-of-type:not[.h-*] 			<div class="h-card"><span><img alt="Jane Doe" src="jane.jpeg"/></span></div> 
				.h-x>:only-child>object[data]:only-of-type:not[.h-*] 		<div class="h-card"><span><object data="jane.jpeg"/>Jane Doe</object></span></div> 
			*/
			var value;
			if(!uf.properties.photo) {
				value = this.getImpliedProperty(node, ['img', 'object'], this.getPhotoAttr);
				if(value) {
					// relative to absolute URL
					if(value && value !== '' && this.options.baseUrl !== '' && value.indexOf('://') === -1) {
						value = modules.url.resolve(value, this.options.baseUrl);
					}
					uf.properties.photo = [modules.utils.trim(value)];
				}
			}		
			return uf;
		};
		
		
		/**
		 * apply implied URL rule
		 *
		 * @param  {DOM Node} node
		 * @param  {Object} uf
		 * @return {Object}
		 */		
		modules.Parser.prototype.impliedUrl = function(node, uf) {
			// implied URL rule
			/*
				a.h-x[href]  							<a class="h-card" href="glenn.html">Glenn</a>
				area.h-x[href]  						<area class="h-card" href="glenn.html">Glenn</area>
				.h-x>a[href]:only-of-type:not[.h-*]  	<div class="h-card" ><a href="glenn.html">Glenn</a><p>...</p></div> 
				.h-x>area[href]:only-of-type:not[.h-*]  <div class="h-card" ><area href="glenn.html">Glenn</area><p>...</p></div>
			*/
			var value;
			if(!uf.properties.url) {
				value = this.getImpliedProperty(node, ['a', 'area'], this.getURLAttr);
				if(value) {
					// relative to absolute URL
					if(value && value !== '' && this.options.baseUrl !== '' && value.indexOf('://') === -1) {
						value = modules.url.resolve(value, this.options.baseUrl);
					}
					uf.properties.url = [modules.utils.trim(value)];
				}
			}	
			return uf;
		};
		
		
		/**
		 * apply implied date rule - if there is a time only property try to concat it with any date property
		 *
		 * @param  {DOM Node} node
		 * @param  {Object} uf
		 * @return {Object}
		 */		
		modules.Parser.prototype.impliedDate = function(uf) {
			// implied date rule
			// http://microformats.org/wiki/value-class-pattern#microformats2_parsers
			// http://microformats.org/wiki/microformats2-parsing-issues#implied_date_for_dt_properties_both_mf2_and_backcompat
			var newDate;
			if(uf.times.length > 0 && uf.dates.length > 0) {
				newDate = modules.dates.dateTimeUnion(uf.dates[0][1], uf.times[0][1], this.options.dateFormat);
				uf.properties[this.removePropPrefix(uf.times[0][0])][0] = newDate.toString(this.options.dateFormat);
			}
			// clean-up object
			delete uf.times;
			delete uf.dates;
			return uf;
		};
			
			
		/**
		 * get an implied property value from pre-defined tag/attriubte combinations
		 *
		 * @param  {DOM Node} node
		 * @param  {String} tagList (Array of tags from which an implied value can be pulled)
		 * @param  {String} getAttrFunction (Function which can extract implied value)
		 * @return {String || null}
		 */
		modules.Parser.prototype.getImpliedProperty = function(node, tagList, getAttrFunction) {
			// i.e. img.h-card
			var value = getAttrFunction(node), 
				descendant,
				child;
					
			if(!value) {
				// i.e. .h-card>img:only-of-type:not(.h-card)
				descendant = modules.domUtils.getSingleDescendantOfType( node, tagList);
				if(descendant && this.hasHClass(descendant) === false){
					value = getAttrFunction(descendant);
				}
				if(node.children.length > 0 ){
					// i.e.  .h-card>:only-child>img:only-of-type:not(.h-card)
					child = modules.domUtils.getSingleDescendant(node);
					if(child && this.hasHClass(child) === false){
						descendant = modules.domUtils.getSingleDescendantOfType(child, tagList);
						if(descendant && this.hasHClass(descendant) === false){
							value = getAttrFunction(descendant);
						}
					}
				}
			}
					
			return value;
		};
			
			
		/**
		 * get an implied name value from a node
		 *
		 * @param  {DOM Node} node
		 * @return {String || null}
		 */		
		modules.Parser.prototype.getNameAttr = function(node) {
			var value = modules.domUtils.getAttrValFromTagList(node, ['img','area'], 'alt');
			if(!value) {
				value = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
			}
			return value;
		};
	
	
		/**
		 * get an implied photo value from a node
		 *
		 * @param  {DOM Node} node
		 * @return {String || null}
		 */	
		modules.Parser.prototype.getPhotoAttr = function(node) {
			var value = modules.domUtils.getAttrValFromTagList(node, ['img'], 'src');
			if(!value && modules.domUtils.hasAttributeValue(node, 'class', 'include') === false) {
				value = modules.domUtils.getAttrValFromTagList(node, ['object'], 'data');
			}
			return value;
		};
			
			
		/**
		 * get an implied photo value from a node
		 *
		 * @param  {DOM Node} node
		 * @return {String || null}
		 */		
		modules.Parser.prototype.getURLAttr = function(node) {
			var value = null;
			if(modules.domUtils.hasAttributeValue(node, 'class', 'include') === false){
				
				value = modules.domUtils.getAttrValFromTagList(node, ['a'], 'href');
				if(!value) {
					value = modules.domUtils.getAttrValFromTagList(node, ['area'], 'href');
				}
				
			}
			return value;
		};
		
		
		/**
		 * 
		 *
		 * @param  {DOM Node} node
		 * @param  {Object} uf
		 * @return {Object}
		 */	
		modules.Parser.prototype.impliedValue = function(node, uf, parentClasses){
			
			// intersection of implied name and implied value rules
			if(uf.properties.name) {	
				if(uf.value && parentClasses.root.length > 0 && parentClasses.properties.length === 1){
					uf = this.getAltValue(uf, parentClasses.properties[0][0], 'p-name', uf.properties.name[0]);
				}
			}
			
			// intersection of implied URL and implied value rules
			if(uf.properties.url) {
				if(parentClasses && parentClasses.root.length === 1 && parentClasses.properties.length === 1){
					uf = this.getAltValue(uf, parentClasses.properties[0][0], 'u-url', uf.properties.url[0]);
				}
			}	
			
			// apply alt value
			if(uf.altValue !== null){
				uf.value = uf.altValue.value;
			}
			delete uf.altValue;
	
	
			return uf;
		};
			
		
		/**
		 * get alt value based on rules about parent property prefix
		 *
		 * @param  {Object} uf
		 * @param  {String} parentPropertyName
		 * @param  {String} propertyName
		 * @param  {String} value
		 * @return {Object}
		 */	
		modules.Parser.prototype.getAltValue = function(uf, parentPropertyName, propertyName, value){
			if(uf.value && !uf.altValue){
				// first p-name of the h-* child
				if(modules.utils.startWith(parentPropertyName,'p-') && propertyName === 'p-name'){
					uf.altValue = {name: propertyName, value: value};
				}
				// if it's an e-* property element
				if(modules.utils.startWith(parentPropertyName,'e-') && modules.utils.startWith(propertyName,'e-')){
					uf.altValue = {name: propertyName, value: value};
				}
				// if it's an u-* property element
				if(modules.utils.startWith(parentPropertyName,'u-') && propertyName === 'u-url'){
					uf.altValue = {name: propertyName, value: value};
				}
			}
			return uf;
		};
		
		
		/**
		 * if a h-feed does not have a title use the title tag of a page
		 *
		 * @param  {Object} uf
		 * @return {Object}
		 */	
		modules.Parser.prototype.impliedhFeedTitle = function( uf ){
			if(uf.type && uf.type.indexOf('h-feed') > -1){
				// has no name property
				if(uf.properties.name === undefined || uf.properties.name[0] === '' ){
					// use the text from the title tag
					var title = modules.domUtils.querySelector(this.document, 'title');
					if(title){
						uf.properties.name = [modules.domUtils.textContent(title)];
					}
				}
			}
			return uf;
		};
		
		
		
	    /**
		 * implied Geo from pattern <abbr class="p-geo" title="37.386013;-122.082932">
		 *
		 * @param  {Object} uf
		 * @return {Object}
		 */	
		modules.Parser.prototype.impliedGeo = function( uf ){
			var geoPair,
				parts,
				longitude,
				latitude,
				valid = true;
			
			if(uf.type && uf.type.indexOf('h-geo') > -1){
				
				// has no latitude or longitude property
				if(uf.properties.latitude === undefined || uf.properties.longitude === undefined ){

					geoPair = (uf.properties.name)? uf.properties.name[0] : null;
					geoPair = (!geoPair && uf.properties.value)? uf.properties.value : geoPair;
					
					if(geoPair){
						// allow for the use of a ';' as in microformats and also ',' as in Geo URL
						geoPair = geoPair.replace(';',',');
						
						// has sep char
						if(geoPair.indexOf(',') > -1 ){
							parts = geoPair.split(',');
							
							// only correct if we have two or more parts
							if(parts.length > 1){

								// latitude no value outside the range -90 or 90 
								latitude = parseFloat( parts[0] );
								if(modules.utils.isNumber(latitude) && latitude > 90 || latitude < -90){
									valid = false;
								}
								
								// longitude no value outside the range -180 to 180
								longitude = parseFloat( parts[1] );
								if(modules.utils.isNumber(longitude) && longitude > 180 || longitude < -180){
									valid = false;
								}
								
								if(valid){
									uf.properties.latitude = [latitude];
									uf.properties.longitude  = [longitude];
								}
							}
							
						}
					}
				}
			}
			return uf;
		};
		
		
		/**
		 * if a backwards compat built structure has no properties add name through this.impliedName
		 *
		 * @param  {Object} uf
		 * @return {Object}
		 */	
		modules.Parser.prototype.impliedBackwardComp = function(node, uf, parentClasses){
			
			// look for pattern in parent classes like "p-geo h-geo"
			// these are structures built from backwards compat parsing of geo
			if(parentClasses.root.length === 1 && parentClasses.properties.length === 1) {
				if(parentClasses.root[0].replace('h-','') === this.removePropPrefix(parentClasses.properties[0][0])) {
					
					// if microformat has no properties apply the impliedName rule to get value from containing node
					// this will get value from html such as <abbr class="geo" title="30.267991;-97.739568">Brighton</abbr>
					if( modules.utils.hasProperties(uf.properties) === false ){
						uf = this.impliedName( node, uf );
					}
				}
			}
			
			return uf;
		};
		
		
	
	}


	// check parser module is loaded
	if(modules.Parser){
	
		
		/**
		 * appends clones of include Nodes into the DOM structure
		 *
		 * @param  {DOM node} rootNode
		 */	
		modules.Parser.prototype.addIncludes = function(rootNode) {
			this.addAttributeIncludes(rootNode, 'itemref');
			this.addAttributeIncludes(rootNode, 'headers');
			this.addClassIncludes(rootNode);
		};
	
		
		/**
		 * appends clones of include Nodes into the DOM structure for attribute based includes
		 *
		 * @param  {DOM node} rootNode
		 * @param  {String} attributeName
		 */
		modules.Parser.prototype.addAttributeIncludes = function(rootNode, attributeName) {
			var arr,
				idList,
				i,
				x,
				z,
				y;
	
			arr = modules.domUtils.getNodesByAttribute(rootNode, attributeName);
			x = 0;
			i = arr.length;
			while(x < i) {
				idList = modules.domUtils.getAttributeList(arr[x], attributeName);
				if(idList) {
					z = 0;
					y = idList.length;
					while(z < y) {
						this.apppendInclude(arr[x], idList[z]);
						z++;
					}
				}
				x++;
			}
		};
	
		
		/**
		 * appends clones of include Nodes into the DOM structure for class based includes
		 *
		 * @param  {DOM node} rootNode
		 */
		modules.Parser.prototype.addClassIncludes = function(rootNode) {
			var id,
				arr,
				x = 0,
				i;
	
			arr = modules.domUtils.getNodesByAttributeValue(rootNode, 'class', 'include');
			i = arr.length;
			while(x < i) {
				id = modules.domUtils.getAttrValFromTagList(arr[x], ['a'], 'href');
				if(!id) {
					id = modules.domUtils.getAttrValFromTagList(arr[x], ['object'], 'data');
				}
				this.apppendInclude(arr[x], id);
				x++;
			}
		};
	
	
		/**
		 * appends a clone of an include into another Node using Id
		 *
		 * @param  {DOM node} rootNode
		 * @param  {Stringe} id
		 */
		modules.Parser.prototype.apppendInclude = function(node, id){
			var include,
				clone;
	
			id = modules.utils.trim(id.replace('#', ''));
			include = modules.domUtils.getElementById(this.document, id);
			if(include) {
				clone = modules.domUtils.clone(include);
				this.markIncludeChildren(clone);
				modules.domUtils.appendChild(node, clone);
			}
		};
	
		
		/**
		 * adds an attribute marker to all the child microformat roots 
		 *
		 * @param  {DOM node} rootNode
		 */ 
		modules.Parser.prototype.markIncludeChildren = function(rootNode) {
			var arr,
				x,
				i;
	
			// loop the array and add the attribute
			arr = this.findRootNodes(rootNode);
			x = 0;
			i = arr.length;
			modules.domUtils.setAttribute(rootNode, 'data-include', 'true');
			modules.domUtils.setAttribute(rootNode, 'style', 'display:none');
			while(x < i) {
				modules.domUtils.setAttribute(arr[x], 'data-include', 'true');
				x++;
			}
		};
		
		
		/**
		 * removes all appended include clones from DOM 
		 *
		 * @param  {DOM node} rootNode
		 */ 
		modules.Parser.prototype.removeIncludes = function(rootNode){
			var arr,
				i;
	
			// remove all the items that were added as includes
			arr = modules.domUtils.getNodesByAttribute(rootNode, 'data-include');
			i = arr.length;
			while(i--) {
				modules.domUtils.removeChild(rootNode,arr[i]);
			}
		};
	
		
	}


	// check parser module is loaded
	if(modules.Parser){
	
		/**
		 * finds rel=* structures
		 *
		 * @param  {DOM node} rootNode
		 * @return {Object}
		 */
		modules.Parser.prototype.findRels = function(rootNode) {
			var out = {
					'items': [],
					'rels': {},
					'rel-urls': {}
				},
				x,
				i,
				y,
				z,
				relList,
				items,
				item,
				value,
				arr;
	
			arr = modules.domUtils.getNodesByAttribute(rootNode, 'rel');
			x = 0;
			i = arr.length;
			while(x < i) {
				relList = modules.domUtils.getAttribute(arr[x], 'rel');
	
				if(relList) {
					items = relList.split(' ');
					
					
					// add rels
					z = 0;
					y = items.length;
					while(z < y) {
						item = modules.utils.trim(items[z]);
	
						// get rel value
						value = modules.domUtils.getAttrValFromTagList(arr[x], ['a', 'area'], 'href');
						if(!value) {
							value = modules.domUtils.getAttrValFromTagList(arr[x], ['link'], 'href');
						}
	
						// create the key
						if(!out.rels[item]) {
							out.rels[item] = [];
						}
	
						if(typeof this.options.baseUrl === 'string' && typeof value === 'string') {
					
							var resolved = modules.url.resolve(value, this.options.baseUrl);
							// do not add duplicate rels - based on resolved URLs
							if(out.rels[item].indexOf(resolved) === -1){
								out.rels[item].push( resolved );
							}
						}
						z++;
					}
					
					
					var url = null;
					if(modules.domUtils.hasAttribute(arr[x], 'href')){
						url = modules.domUtils.getAttribute(arr[x], 'href');
						if(url){
							url = modules.url.resolve(url, this.options.baseUrl );
						}
					}
	
					
					// add to rel-urls
					var relUrl = this.getRelProperties(arr[x]);
					relUrl.rels = items;
					// // do not add duplicate rel-urls - based on resolved URLs
					if(url && out['rel-urls'][url] === undefined){
						out['rel-urls'][url] = relUrl;
					}
	
			
				}
				x++;
			}
			return out;
		};
		
		
		/**
		 * gets the properties of a rel=*
		 *
		 * @param  {DOM node} node
		 * @return {Object}
		 */
		modules.Parser.prototype.getRelProperties = function(node){
			var obj = {};
			
			if(modules.domUtils.hasAttribute(node, 'media')){
				obj.media = modules.domUtils.getAttribute(node, 'media');
			}
			if(modules.domUtils.hasAttribute(node, 'type')){
				obj.type = modules.domUtils.getAttribute(node, 'type');
			}
			if(modules.domUtils.hasAttribute(node, 'hreflang')){
				obj.hreflang = modules.domUtils.getAttribute(node, 'hreflang');
			}
			if(modules.domUtils.hasAttribute(node, 'title')){
				obj.title = modules.domUtils.getAttribute(node, 'title');
			}
			if(modules.utils.trim(this.getPValue(node, false)) !== ''){
				obj.text = this.getPValue(node, false);
			}	
			
			return obj;
		};
		
		
		/**
		 * finds any alt rel=* mappings for a given node/microformat
		 *
		 * @param  {DOM node} node
		 * @param  {String} ufName
		 * @return {String || undefined}
		 */
		modules.Parser.prototype.findRelImpied = function(node, ufName) {
			var out,
				map,
				i;
	
			map = this.getMapping(ufName);
			if(map) {
				for(var key in map.properties) {
					if (map.properties.hasOwnProperty(key)) {
						var prop = map.properties[key],
							propName = (prop.map) ? prop.map : 'p-' + key,
							relCount = 0;
		
						// is property an alt rel=* mapping 
						if(prop.relAlt && modules.domUtils.hasAttribute(node, 'rel')) {
							i = prop.relAlt.length;
							while(i--) {
								if(modules.domUtils.hasAttributeValue(node, 'rel', prop.relAlt[i])) {
									relCount++;
								}
							}
							if(relCount === prop.relAlt.length) {
								out = propName;
							}
						}
					}
				}
			}
			return out;
		};
		
		
		/**
		 * returns whether a node or its children has rel=* microformat
		 *
		 * @param  {DOM node} node
		 * @return {Boolean}
		 */
		modules.Parser.prototype.hasRel = function(node) {
			return (this.countRels(node) > 0);
		};
		
		
		/**
		 * returns the number of rel=* microformats
		 *
		 * @param  {DOM node} node
		 * @return {Int}
		 */
		modules.Parser.prototype.countRels = function(node) {
			if(node){
				return modules.domUtils.getNodesByAttribute(node, 'rel').length;
			}
			return 0;
		};
	
	
		
	}


	modules.utils = {
		
		/**
		 * is the object a string
		 *
		 * @param  {Object} obj
		 * @return {Boolean}
		 */
		isString: function( obj ) {
			return typeof( obj ) === 'string';
		},
		
		/**
		 * is the object a number
		 *
		 * @param  {Object} obj
		 * @return {Boolean}
		 */
		isNumber: function( obj ) {
			return !isNaN(parseFloat( obj )) && isFinite( obj );
		},
		
		
		/**
		 * is the object an array
		 *
		 * @param  {Object} obj
		 * @return {Boolean}
		 */
		isArray: function( obj ) {
			return obj && !( obj.propertyIsEnumerable( 'length' ) ) && typeof obj === 'object' && typeof obj.length === 'number';
		},
		
		
		/**
		 * is the object a function
		 *
		 * @param  {Object} obj
		 * @return {Boolean}
		 */
		isFunction: function(obj) {
			return !!(obj && obj.constructor && obj.call && obj.apply);
		},
	
	
		/**
		 * does the text start with a test string
		 *
		 * @param  {String} text
		 * @param  {String} test
		 * @return {Boolean}
		 */
		startWith: function( text, test ) {
			return(text.indexOf(test) === 0);
		},
	
		
		/**
		 * removes spaces at front and back of text
		 *
		 * @param  {String} text
		 * @return {String}
		 */
		trim: function( text ) {
			if(text && this.isString(text)){
				return (text.trim())? text.trim() : text.replace(/^\s+|\s+$/g, '');
			}else{
				return '';
			}
		},
		
		
		/**
		 * replaces a character in text
		 *
		 * @param  {String} text
		 * @param  {Int} index
		 * @param  {String} character
		 * @return {String}
		 */
		replaceCharAt: function( text, index, character ) {
			if(text && text.length > index){
			   return text.substr(0, index) + character + text.substr(index+character.length); 
			}else{
				return text;
			}
		},
		
		
		/**
		 * removes whitespace, tabs and returns from start and end of text
		 *
		 * @param  {String} text
		 * @return {String}
		 */
		trimWhitespace: function( text ){
			if(text && text.length){
				var i = text.length,
					x = 0;
				
				// turn all whitespace chars at end into spaces
				while (i--) {
					if(this.isOnlyWhiteSpace(text[i])){
						text = this.replaceCharAt( text, i, ' ' );
					}else{
						break;
					}
				}
				
				// turn all whitespace chars at start into spaces
				i = text.length;
				while (x < i) {
					if(this.isOnlyWhiteSpace(text[x])){
						text = this.replaceCharAt( text, i, ' ' );
					}else{
						break;
					}
					x++;
				}
			}
			return this.trim(text);
		},
	
	
		/**
		 * does text only contain whitespace characters
		 *
		 * @param  {String} text
		 * @return {Boolean}
		 */
		isOnlyWhiteSpace: function( text ){
			return !(/[^\t\n\r ]/.test( text ));
		},
		
		
		/**
		 * removes whitespace from text (leaves a single space)
		 *
		 * @param  {String} text
		 * @return {Sring}
		 */
		collapseWhiteSpace: function( text ){
			return text.replace(/[\t\n\r ]+/g, ' ');
		},
	
	
		/**
		 * does an object have any of its own properties
		 *
		 * @param  {Object} obj
		 * @return {Boolean}
		 */ 
		hasProperties: function( obj ) {
			var key;
			for(key in obj) {
				if( obj.hasOwnProperty( key ) ) {
					return true;
				}
			}
			return false;
		},
		
		
		/**
		 * a sort function - to sort objects in an array by a given property
		 *
		 * @param  {String} property
		 * @param  {Boolean} reverse
		 * @return {Int}
		 */ 
		sortObjects: function(property, reverse) {
			reverse = (reverse) ? -1 : 1;
			return function (a, b) {
				a = a[property];
				b = b[property];
				if (a < b) {
					return reverse * -1;
				}
				if (a > b) {
					return reverse * 1;
				}
				return 0;
			};
		}
		
	};


	modules.domUtils = {

		// blank objects for DOM
		document: null,
		rootNode: null,


	     /**
		 * gets DOMParser object
		 *
         * @return {Object || undefined}
		 */
        getDOMParser: function () {
            if (typeof DOMParser === undefined) {
                try {
                    return Components.classes["@mozilla.org/xmlextras/domparser;1"]
                        .createInstance(Components.interfaces.nsIDOMParser);
                } catch (e) {
                    return;
                }
            } else {
                return new DOMParser();
            }
        },


	     /**
		 * configures what are the base DOM objects for parsing
		 *
		 * @param  {Object} options
		 * @return {DOM Node} node
		 */
		getDOMContext: function( options ){

			// if a node is passed
			if(options.node){
				this.rootNode = options.node;
			}


			// if a html string is passed
			if(options.html){
				//var domParser = new DOMParser();
                var domParser = this.getDOMParser();
       			this.rootNode = domParser.parseFromString( options.html, 'text/html' );
			}


			// find top level document from rootnode
			if(this.rootNode !== null){
				if(this.rootNode.nodeType === 9){
					this.document = this.rootNode;
					this.rootNode = modules.domUtils.querySelector(this.rootNode, 'html');
				}else{
					// if it's DOM node get parent DOM Document
					this.document = modules.domUtils.ownerDocument(this.rootNode);
				}
			}


			// use global document object
			if(!this.rootNode && document){
				this.rootNode = modules.domUtils.querySelector(document, 'html');
				this.document = document;
			}


			if(this.rootNode && this.document){
				return {document: this.document, rootNode: this.rootNode};
			}

			return {document: null, rootNode: null};
		},



		/**
		* gets the first DOM node
		*
		* @param  {Dom Document}
		* @return {DOM Node} node
		*/
		getTopMostNode: function( node ){
			//var doc = this.ownerDocument(node);
			//if(doc && doc.nodeType && doc.nodeType === 9 && doc.documentElement){
			//	return doc.documentElement;
			//}
			return node;
		},



		 /**
		 * abstracts DOM ownerDocument
		 *
		 * @param  {DOM Node} node
		 * @return {Dom Document}
		 */
		ownerDocument: function(node){
			return node.ownerDocument;
		},


		/**
		 * abstracts DOM textContent
		 *
		 * @param  {DOM Node} node
		 * @return {String}
		 */
		textContent: function(node){
			if(node.textContent){
				return node.textContent;
			}else if(node.innerText){
				return node.innerText;
			}
			return '';
		},


		/**
		 * abstracts DOM innerHTML
		 *
		 * @param  {DOM Node} node
		 * @return {String}
		 */
		innerHTML: function(node){
			return node.innerHTML;
		},


		/**
		 * abstracts DOM hasAttribute
		 *
		 * @param  {DOM Node} node
		 * @param  {String} attributeName
		 * @return {Boolean}
		 */
		hasAttribute: function(node, attributeName) {
			return node.hasAttribute(attributeName);
		},


		/**
		 * does an attribute contain a value
		 *
		 * @param  {DOM Node} node
		 * @param  {String} attributeName
		 * @param  {String} value
		 * @return {Boolean}
		 */
		hasAttributeValue: function(node, attributeName, value) {
			return (this.getAttributeList(node, attributeName).indexOf(value) > -1);
		},


		/**
		 * abstracts DOM getAttribute
		 *
		 * @param  {DOM Node} node
		 * @param  {String} attributeName
		 * @return {String || null}
		 */
		getAttribute: function(node, attributeName) {
			return node.getAttribute(attributeName);
		},


		/**
		 * abstracts DOM setAttribute
		 *
		 * @param  {DOM Node} node
		 * @param  {String} attributeName
		 * @param  {String} attributeValue
		 */
		setAttribute: function(node, attributeName, attributeValue){
			node.setAttribute(attributeName, attributeValue);
		},


		/**
		 * abstracts DOM removeAttribute
		 *
		 * @param  {DOM Node} node
		 * @param  {String} attributeName
		 */
		removeAttribute: function(node, attributeName) {
			node.removeAttribute(attributeName);
		},


		/**
		 * abstracts DOM getElementById
		 *
		 * @param  {DOM Node || DOM Document} node
		 * @param  {String} id
		 * @return {DOM Node}
		 */
		getElementById: function(docNode, id) {
			return docNode.querySelector( '#' + id );
		},


		/**
		 * abstracts DOM querySelector
		 *
		 * @param  {DOM Node || DOM Document} node
		 * @param  {String} selector
		 * @return {DOM Node}
		 */
		querySelector: function(docNode, selector) {
			return docNode.querySelector( selector );
		},


		/**
		 * get value of a Node attribute as an array
		 *
		 * @param  {DOM Node} node
		 * @param  {String} attributeName
		 * @return {Array}
		 */
		getAttributeList: function(node, attributeName) {
			var out = [],
				attList;

			attList = node.getAttribute(attributeName);
			if(attList && attList !== '') {
				if(attList.indexOf(' ') > -1) {
					out = attList.split(' ');
				} else {
					out.push(attList);
				}
			}
			return out;
		},


		/**
		 * gets all child nodes with a given attribute
		 *
		 * @param  {DOM Node} node
		 * @param  {String} attributeName
		 * @return {NodeList}
		 */
		getNodesByAttribute: function(node, attributeName) {
			var selector = '[' + attributeName + ']';
			return node.querySelectorAll(selector);
		},


		/**
		 * gets all child nodes with a given attribute containing a given value
		 *
		 * @param  {DOM Node} node
		 * @param  {String} attributeName
		 * @return {DOM NodeList}
		 */
		getNodesByAttributeValue: function(rootNode, name, value) {
			var arr = [],
				x = 0,
				i,
				out = [];

			arr = this.getNodesByAttribute(rootNode, name);
			if(arr) {
				i = arr.length;
				while(x < i) {
					if(this.hasAttributeValue(arr[x], name, value)) {
						out.push(arr[x]);
					}
					x++;
				}
			}
			return out;
		},


		/**
		 * gets attribute value from controlled list of tags
		 *
		 * @param  {Array} tagNames
		 * @param  {String} attributeName
		 * @return {String || null}
		 */
		getAttrValFromTagList: function(node, tagNames, attributeName) {
			var i = tagNames.length;

			while(i--) {
				if(node.tagName.toLowerCase() === tagNames[i]) {
					var attrValue = this.getAttribute(node, attributeName);
					if(attrValue && attrValue !== '') {
						return attrValue;
					}
				}
			}
			return null;
		},


	   /**
		 * get node if it has no siblings. CSS equivalent is :only-child
		 *
		 * @param  {DOM Node} rootNode
		 * @param  {Array} tagNames
		 * @return {DOM Node || null}
		 */
		getSingleDescendant: function(node){
			return this.getDescendant( node, null, false );
		},


        /**
		 * get node if it has no siblings of the same type. CSS equivalent is :only-of-type
		 *
		 * @param  {DOM Node} rootNode
		 * @param  {Array} tagNames
		 * @return {DOM Node || null}
		 */
		getSingleDescendantOfType: function(node, tagNames){
			return this.getDescendant( node, tagNames, true );
		},


	    /**
		 * get child node limited by presence of siblings - either CSS :only-of-type or :only-child
		 *
		 * @param  {DOM Node} rootNode
		 * @param  {Array} tagNames
		 * @return {DOM Node || null}
		 */
		getDescendant: function( node, tagNames, onlyOfType ){
			var i = node.children.length,
				countAll = 0,
				countOfType = 0,
				child,
				out = null;

			while(i--) {
				child = node.children[i];
				if(child.nodeType === 1) {
					if(tagNames){
						// count just only-of-type
						if(this.hasTagName(child, tagNames)){
							out = child;
							countOfType++;
						}
					}else{
						// count all elements
						out = child;
						countAll++;
					}
				}
			}
			if(onlyOfType === true){
				return (countOfType === 1)? out : null;
			}else{
				return (countAll === 1)? out : null;
			}
		},


	   /**
		 * is a node one of a list of tags
		 *
		 * @param  {DOM Node} rootNode
		 * @param  {Array} tagNames
		 * @return {Boolean}
		 */
		hasTagName: function(node, tagNames){
			var i = tagNames.length;
			while(i--) {
				if(node.tagName.toLowerCase() === tagNames[i]) {
					return true;
				}
			}
			return false;
		},


	   /**
		 * abstracts DOM appendChild
		 *
		 * @param  {DOM Node} node
		 * @param  {DOM Node} childNode
		 * @return {DOM Node}
		 */
		appendChild: function(node, childNode){
			return node.appendChild(childNode);
		},


	   /**
		 * abstracts DOM removeChild
		 *
		 * @param  {DOM Node} childNode
		 * @return {DOM Node || null}
		 */
		removeChild: function(childNode){
			if (childNode.parentNode) {
				return childNode.parentNode.removeChild(childNode);
			}else{
				return null;
			}
		},


		/**
		 * abstracts DOM cloneNode
		 *
		 * @param  {DOM Node} node
		 * @return {DOM Node}
		 */
		clone: function(node) {
			var newNode = node.cloneNode(true);
			newNode.removeAttribute('id');
			return newNode;
		},


		/**
		 * gets the text of a node
		 *
		 * @param  {DOM Node} node
		 * @return {String}
		 */
		getElementText: function( node ){
			if(node && node.data){
				return node.data;
			}else{
				return '';
			}
		},


		/**
		 * gets the attributes of a node - ordered by sequence in html
		 *
		 * @param  {DOM Node} node
		 * @return {Array}
		 */
		getOrderedAttributes: function( node ){
			var nodeStr = node.outerHTML,
				attrs = [];

			for (var i = 0; i < node.attributes.length; i++) {
				var attr = node.attributes[i];
					attr.indexNum = nodeStr.indexOf(attr.name);

				attrs.push( attr );
			}
			return attrs.sort( modules.utils.sortObjects( 'indexNum' ) );
		},


		/**
		 * decodes html entities in given text
		 *
		 * @param  {DOM Document} doc
		 * @param  String} text
		 * @return {String}
		 */
		decodeEntities: function( doc, text ){
			//return text;
			return doc.createTextNode( text ).nodeValue;
		},


		/**
		 * clones a DOM document
		 *
		 * @param  {DOM Document} document
		 * @return {DOM Document}
		 */
		cloneDocument: function( document ){
			var newNode,
				newDocument = null;

			if( this.canCloneDocument( document )){
				newDocument = document.implementation.createHTMLDocument('');
				newNode = newDocument.importNode( document.documentElement, true );
				newDocument.replaceChild(newNode, newDocument.querySelector('html'));
			}
			return (newNode && newNode.nodeType && newNode.nodeType === 1)? newDocument : document;
		},


		/**
		 * can environment clone a DOM document
		 *
		 * @param  {DOM Document} document
		 * @return {Boolean}
		 */
		canCloneDocument: function( document ){
			return (document && document.importNode && document.implementation && document.implementation.createHTMLDocument);
		},


		/**
		 * get the child index of a node. Used to create a node path
		 *
		 *   @param  {DOM Node} node
		 *   @return {Int}
		 */
		getChildIndex: function (node) {
		  	var parent = node.parentNode,
		  		i = -1,
		  		child;
	  		while (parent && (child = parent.childNodes[++i])){
				 if (child === node){
					 return i;
				 }
			}
	  		return -1;
		},


		/**
		 * get a node's path
		 *
		 *   @param  {DOM Node} node
		 *   @return {Array}
		 */
		getNodePath: function  (node) {
		  	var parent = node.parentNode,
			  	path = [],
			  	index = this.getChildIndex(node);

		  if(parent && (path = this.getNodePath(parent))){
			   if(index > -1){
				   path.push(index);
			   }
		  }
		  return path;
		},


		/**
		 * get a node from a path.
		 *
		 *   @param  {DOM document} document
		 *   @param  {Array} path
		 *   @return {DOM Node}
		 */
		getNodeByPath: function (document, path) {
		  	var node = document.documentElement,
		  		i = 0,
		  		index;
		  while ((index = path[++i]) > -1){
			  node = node.childNodes[index];
		  }
		  return node;
		},


		/**
		* get an array/nodeList of child nodes
		*
		*   @param  {DOM node} node
		*   @return {Array}
		*/
		getChildren: function( node ){
			return node.children;
		},


		/**
		* create a node
		*
		*   @param  {String} tagName
		*   @return {DOM node}
		*/
		createNode: function( tagName ){
			return this.document.createElement(tagName);
		},


		/**
		* create a node with text content
		*
		*   @param  {String} tagName
		*   @param  {String} text
		*   @return {DOM node}
		*/
		createNodeWithText: function( tagName, text ){
			var node = this.document.createElement(tagName);
			node.innerHTML = text;
			return node;
		}



	};


	modules.url = {


		/**
		 * creates DOM objects needed to resolve URLs
		 */
        init: function(){
            //this._domParser = new DOMParser();
            this._domParser = modules.domUtils.getDOMParser();
            // do not use a head tag it does not work with IE9
            this._html = '<base id="base" href=""></base><a id="link" href=""></a>';
            this._nodes = this._domParser.parseFromString( this._html, 'text/html' );
            this._baseNode =  modules.domUtils.getElementById(this._nodes,'base');
            this._linkNode =  modules.domUtils.getElementById(this._nodes,'link');
        },


		/**
		 * resolves url to absolute version using baseUrl
		 *
		 * @param  {String} url
		 * @param  {String} baseUrl
		 * @return {String}
		 */
		resolve: function(url, baseUrl) {
			// use modern URL web API where we can
			if(modules.utils.isString(url) && modules.utils.isString(baseUrl) && url.indexOf('://') === -1){
				// this try catch is required as IE has an URL object but no constuctor support
				// http://glennjones.net/articles/the-problem-with-window-url
				try {
					var resolved = new URL(url, baseUrl).toString();
					// deal with early Webkit not throwing an error - for Safari
					if(resolved === '[object URL]'){
						resolved = URI.resolve(baseUrl, url);
					}
					return resolved;
				}catch(e){
                    // otherwise fallback to DOM
                    if(this._domParser === undefined){
                        this.init();
                    }

                    // do not use setAttribute it does not work with IE9
                    this._baseNode.href = baseUrl;
                    this._linkNode.href = url;

                    // dont use getAttribute as it returns orginal value not resolved
                    return this._linkNode.href;
				}
			}else{
				if(modules.utils.isString(url)){
					return url;
				}
				return '';
			}
		},

	};


	/**
	 * constructor
	 * parses text to find just the date element of an ISO date/time string i.e. 2008-05-01
	 *
	 * @param  {String} dateString
	 * @param  {String} format
	 * @return {String}
	 */ 
	modules.ISODate = function ( dateString, format ) {
		this.clear();
	
		this.format = (format)? format : 'auto'; // auto or W3C or RFC3339 or HTML5
		this.setFormatSep();
	
		// optional should be full iso date/time string 
		if(arguments[0]) {
			this.parse(dateString, format);
		}
	};
	

	modules.ISODate.prototype = {
		
		
		/**
		 * clear all states
		 *
		 */ 
		clear: function(){
			this.clearDate();
			this.clearTime();
			this.clearTimeZone();
			this.setAutoProfileState();
		},
		
		
		/**
		 * clear date states
		 *
		 */ 
		clearDate: function(){
			this.dY = -1;
			this.dM = -1;
			this.dD = -1;
			this.dDDD = -1;
		},
		
		
		/**
		 * clear time states
		 *
		 */ 
		clearTime: function(){
			this.tH = -1;
			this.tM = -1;
			this.tS = -1;
			this.tD = -1;
		},
		
		
		/**
		 * clear timezone states
		 *
		 */ 
		clearTimeZone: function(){
			this.tzH = -1;
			this.tzM = -1;
			this.tzPN = '+';
			this.z = false;
		},
		
		
		/**
		 * resets the auto profile state
		 *
		 */ 
		setAutoProfileState: function(){
			this.autoProfile = {
			   sep: 'T',
			   dsep: '-',
			   tsep: ':',
			   tzsep: ':',
			   tzZulu: 'Z'
			};
		},
		
	  
		/**
		 * parses text to find ISO date/time string i.e. 2008-05-01T15:45:19Z
		 *
		 * @param  {String} dateString
		 * @param  {String} format
		 * @return {String}
		 */ 
		parse: function( dateString, format ) {
			this.clear();
			
			var parts = [],
				tzArray = [],
				position = 0,
				datePart = '',
				timePart = '',
				timeZonePart = '';
				
			if(format){
				this.format = format;
			}
			
	
			
			// discover date time separtor for auto profile
			// Set to 'T' by default
			if(dateString.indexOf('t') > -1) {
				this.autoProfile.sep = 't';
			}
			if(dateString.indexOf('z') > -1) {
				this.autoProfile.tzZulu = 'z';
			}
			if(dateString.indexOf('Z') > -1) {
				this.autoProfile.tzZulu = 'Z';
			}
			if(dateString.toUpperCase().indexOf('T') === -1) {
				this.autoProfile.sep = ' ';
			}     
	
	
			dateString = dateString.toUpperCase().replace(' ','T');
	
			// break on 'T' divider or space
			if(dateString.indexOf('T') > -1) {
				parts = dateString.split('T');
				datePart = parts[0];
				timePart = parts[1];
	
				// zulu UTC                 
				if(timePart.indexOf( 'Z' ) > -1) {
					this.z = true;
				}
	
				// timezone
				if(timePart.indexOf( '+' ) > -1 || timePart.indexOf( '-' ) > -1) {
					tzArray = timePart.split( 'Z' ); // incase of incorrect use of Z
					timePart = tzArray[0];
					timeZonePart = tzArray[1];
	
					// timezone
					if(timePart.indexOf( '+' ) > -1 || timePart.indexOf( '-' ) > -1) {
						position = 0;
	
						if(timePart.indexOf( '+' ) > -1) {
							position = timePart.indexOf( '+' );
						} else {
							position = timePart.indexOf( '-' );
						}
	
						timeZonePart = timePart.substring( position, timePart.length );
						timePart = timePart.substring( 0, position );
					}
				}
	
			} else {
				datePart = dateString;
			}
	
			if(datePart !== '') {
				this.parseDate( datePart );
				if(timePart !== '') {
					this.parseTime( timePart );
					if(timeZonePart !== '') {
						this.parseTimeZone( timeZonePart );
					}
				}
			}
			return this.toString( format );
		},
	
		
		/**
		 * parses text to find just the date element of an ISO date/time string i.e. 2008-05-01
		 *
		 * @param  {String} dateString
		 * @param  {String} format
		 * @return {String}
		 */ 
		parseDate: function( dateString, format ) {
			this.clearDate();
			
			var parts = [];
				
			// discover timezone separtor for auto profile // default is ':'
			if(dateString.indexOf('-') === -1) {
				this.autoProfile.tsep = '';
			}  
	
			// YYYY-DDD
			parts = dateString.match( /(\d\d\d\d)-(\d\d\d)/ );
			if(parts) {
				if(parts[1]) {
					this.dY = parts[1];
				}
				if(parts[2]) {
					this.dDDD = parts[2];
				}
			}
	
			if(this.dDDD === -1) {
				// YYYY-MM-DD ie 2008-05-01 and YYYYMMDD ie 20080501
				parts = dateString.match( /(\d\d\d\d)?-?(\d\d)?-?(\d\d)?/ );
				if(parts[1]) {
					this.dY = parts[1];
				}
				if(parts[2]) {
					this.dM = parts[2];
				}
				if(parts[3]) {
					this.dD = parts[3];
				}
			}
			return this.toString(format);
		},
	
	
		/**
		 * parses text to find just the time element of an ISO date/time string i.e. 13:30:45
		 *
		 * @param  {String} timeString
		 * @param  {String} format
		 * @return {String}
		 */ 
		parseTime: function( timeString, format ) {
			this.clearTime();
			var parts = [];
				
			// discover date separtor for auto profile // default is ':'
			if(timeString.indexOf(':') === -1) {
				this.autoProfile.tsep = '';
			}      
	
			// finds timezone HH:MM:SS and HHMMSS  ie 13:30:45, 133045 and 13:30:45.0135
			parts = timeString.match( /(\d\d)?:?(\d\d)?:?(\d\d)?.?([0-9]+)?/ );
			if(parts[1]) {
				this.tH = parts[1];
			}
			if(parts[2]) {
				this.tM = parts[2];
			}
			if(parts[3]) {
				this.tS = parts[3];
			}
			if(parts[4]) {
				this.tD = parts[4];
			}
			return this.toTimeString(format);
		},
	
		
		/**
		 * parses text to find just the time element of an ISO date/time string i.e. +08:00
		 *
		 * @param  {String} timeString
		 * @param  {String} format
		 * @return {String}
		 */ 
		parseTimeZone: function( timeString, format ) {
			this.clearTimeZone();
			var parts = [];
			
			if(timeString.toLowerCase() === 'z'){
				this.z = true;
				// set case for z
				this.autoProfile.tzZulu = (timeString === 'z')? 'z' : 'Z';
			}else{
				
				// discover timezone separtor for auto profile // default is ':'
				if(timeString.indexOf(':') === -1) {
					this.autoProfile.tzsep = '';
				}   
			   
				// finds timezone +HH:MM and +HHMM  ie +13:30 and +1330
				parts = timeString.match( /([\-\+]{1})?(\d\d)?:?(\d\d)?/ );
				if(parts[1]) {
					this.tzPN = parts[1];
				}
				if(parts[2]) {
					this.tzH = parts[2];
				}
				if(parts[3]) {
					this.tzM = parts[3];
				} 
				
	  
			}
			this.tzZulu = 'z';    
			return this.toTimeString( format );
		},
		
		
		/**
		 * returns ISO date/time string in W3C Note, RFC 3339, HTML5, or auto profile
		 *
		 * @param  {String} format
		 * @return {String}
		 */ 
		toString: function( format ) {
			var output = '';
	
			if(format){
				this.format = format;
			}
			this.setFormatSep();
	
			if(this.dY  > -1) {
				output = this.dY;
				if(this.dM > 0 && this.dM < 13) {
					output += this.dsep + this.dM;
					if(this.dD > 0 && this.dD < 32) {
						output += this.dsep + this.dD;
						if(this.tH > -1 && this.tH < 25) {
							output += this.sep + this.toTimeString( format );
						}
					}
				}
				if(this.dDDD > -1) {
					output += this.dsep + this.dDDD;
				}
			} else if(this.tH > -1) {
				output += this.toTimeString( format );
			}
	
			return output;
		},
	
	
		/**
		 * returns just the time string element of an ISO date/time
		 * in W3C Note, RFC 3339, HTML5, or auto profile
		 *
		 * @param  {String} format
		 * @return {String}
		 */ 
		toTimeString: function( format ) {
			var out = '';
	
			if(format){
				this.format = format;
			}
			this.setFormatSep();
			
			// time can only be created with a full date
			if(this.tH) {
				if(this.tH > -1 && this.tH < 25) {
					out += this.tH;
					if(this.tM > -1 && this.tM < 61){
						out += this.tsep + this.tM;
						if(this.tS > -1 && this.tS < 61){
							out += this.tsep + this.tS;
							if(this.tD > -1){
								out += '.' + this.tD;
							}
						}
					}
					
					
			  
					// time zone offset 
					if(this.z) {
						out += this.tzZulu;
					} else {
						if(this.tzH && this.tzH > -1 && this.tzH < 25) {
							out += this.tzPN + this.tzH;
							if(this.tzM > -1 && this.tzM < 61){
								out += this.tzsep + this.tzM;
							}
						}
					}
				}
			}
			return out;
		},
	
	
		/**
		 * set the current profile to W3C Note, RFC 3339, HTML5, or auto profile
		 *
		 */ 
		setFormatSep: function() {
			switch( this.format.toLowerCase() ) {
				case 'rfc3339':
					this.sep = 'T';
					this.dsep = '';
					this.tsep = '';
					this.tzsep = '';
					this.tzZulu = 'Z';
					break;
				case 'w3c':
					this.sep = 'T';
					this.dsep = '-';
					this.tsep = ':';
					this.tzsep = ':';
					this.tzZulu = 'Z';
					break;
				case 'html5':
					this.sep = ' ';
					this.dsep = '-';
					this.tsep = ':';
					this.tzsep = ':';
					this.tzZulu = 'Z';
					break;
				default:
					// auto - defined by format of input string
					this.sep = this.autoProfile.sep;
					this.dsep = this.autoProfile.dsep;
					this.tsep = this.autoProfile.tsep;
					this.tzsep = this.autoProfile.tzsep;
					this.tzZulu = this.autoProfile.tzZulu;
			}
		},
	
	
		/**
		 * does current data contain a full date i.e. 2015-03-23
		 *
		 * @return {Boolean}
		 */ 
		hasFullDate: function() {
			return(this.dY !== -1 && this.dM !== -1 && this.dD !== -1);
		},
	
	
		/**
		 * does current data contain a minimum date which is just a year number i.e. 2015
		 *
		 * @return {Boolean}
		 */ 
		hasDate: function() {
			return(this.dY !== -1);
		},
	
	
		/**
		 * does current data contain a minimum time which is just a hour number i.e. 13
		 *
		 * @return {Boolean}
		 */     
		hasTime: function() {
			return(this.tH !== -1);
		},
	
		/**
		 * does current data contain a minimum timezone i.e. -1 || +1 || z
		 *
		 * @return {Boolean}
		 */    
		hasTimeZone: function() {
			return(this.tzH !== -1);
		}
	
	};
	
	modules.ISODate.prototype.constructor = modules.ISODate;


	modules.dates = {

		
		/**
		 * does text contain am
		 *
		 * @param  {String} text
		 * @return {Boolean}
		 */
		hasAM: function( text ) {
			text = text.toLowerCase();
			return(text.indexOf('am') > -1 || text.indexOf('a.m.') > -1);
		},
	
	
		/**
		 * does text contain pm
		 *
		 * @param  {String} text
		 * @return {Boolean}
		 */
		hasPM: function( text ) {
			text = text.toLowerCase();
			return(text.indexOf('pm') > -1 || text.indexOf('p.m.') > -1);
		},
	
	
		/**
		 * remove am and pm from text and return it
		 *
		 * @param  {String} text
		 * @return {String}
		 */
		removeAMPM: function( text ) {
			return text.replace('pm', '').replace('p.m.', '').replace('am', '').replace('a.m.', '');
		},
	
	   
	   /**
		 * simple test of whether ISO date string is a duration  i.e.  PY17M or PW12
		 *
		 * @param  {String} text
		 * @return {Boolean}
		 */
		isDuration: function( text ) {
			if(modules.utils.isString( text )){
				text = text.toLowerCase();
				if(modules.utils.startWith(text, 'p') ){
					return true;
				}
			}
			return false;
		},
	
	
	   /**
		 * is text a time or timezone
		 * i.e. HH-MM-SS or z+-HH-MM-SS 08:43 | 15:23:00:0567 | 10:34pm | 10:34 p.m. | +01:00:00 | -02:00 | z15:00 | 0843
		 *
		 * @param  {String} text
		 * @return {Boolean}
		 */ 
		isTime: function( text ) {
			if(modules.utils.isString(text)){
				text = text.toLowerCase();
				text = modules.utils.trim( text );
				// start with timezone char
				if( text.match(':') && ( modules.utils.startWith(text, 'z') || modules.utils.startWith(text, '-')  || modules.utils.startWith(text, '+') )) {
					return true;
				}
				// has ante meridiem or post meridiem
				if( text.match(/^[0-9]/) && 
					( this.hasAM(text) || this.hasPM(text) )) {
					return true;
				}
				// contains time delimiter but not datetime delimiter
				if( text.match(':') && !text.match(/t|\s/) ) {
					return true;
				}
				
				// if it's a number of 2, 4 or 6 chars
				if(modules.utils.isNumber(text)){
					if(text.length === 2 || text.length === 4 || text.length === 6){
						return true;
					}
				}
			}
			return false;
		},
	

		/**
		 * parses a time from text and returns 24hr time string
		 * i.e. 5:34am = 05:34:00 and 1:52:04p.m. = 13:52:04
		 *
		 * @param  {String} text
		 * @return {String}
		 */ 
		parseAmPmTime: function( text ) {
			var out = text,
				times = [];
	
			// if the string has a text : or am or pm
			if(modules.utils.isString(out)) {
				//text = text.toLowerCase();
				text = text.replace(/[ ]+/g, '');
	
				if(text.match(':') || this.hasAM(text) || this.hasPM(text)) {
	
					if(text.match(':')) {
						times = text.split(':');
					} else {
						// single number text i.e. 5pm
						times[0] = text;
						times[0] = this.removeAMPM(times[0]);
					}
					
					// change pm hours to 24hr number
					if(this.hasPM(text)) {
						if(times[0] < 12) {
							times[0] = parseInt(times[0], 10) + 12;
						}
					}
	
					// add leading zero's where needed
					if(times[0] && times[0].length === 1) {
						times[0] = '0' + times[0];
					}
					
					// rejoin text elements together
					if(times[0]) {
						text = times.join(':');
					}
				}
			}
			
			// remove am/pm strings
			return this.removeAMPM(text);
		},
	
	
	   /**
		 * overlays a time on a date to return the union of the two
		 *
		 * @param  {String} date
		 * @param  {String} time
		 * @param  {String} format ( Modules.ISODate profile format )
		 * @return {Object} Modules.ISODate
		 */ 
		dateTimeUnion: function(date, time, format) {
			var isodate = new modules.ISODate(date, format),
				isotime = new modules.ISODate();
	
			isotime.parseTime(this.parseAmPmTime(time), format);
			if(isodate.hasFullDate() && isotime.hasTime()) {
				isodate.tH = isotime.tH;
				isodate.tM = isotime.tM;
				isodate.tS = isotime.tS;
				isodate.tD = isotime.tD;
				return isodate;
			} else {
				if(isodate.hasFullDate()){
					return isodate;
				}
				return new modules.ISODate();
			}
		},
	
	
	   /**
		 * concatenate an array of date and time text fragments to create an ISODate object
		 * used for microformat value and value-title rules
		 *
		 * @param  {Array} arr ( Array of Strings )
		 * @param  {String} format ( Modules.ISODate profile format )
		 * @return {Object} Modules.ISODate
		 */ 
		concatFragments: function (arr, format) {
			var out = new modules.ISODate(),
				i = 0,
				value = '';
			
			// if the fragment already contains a full date just return it once 
			if(arr[0].toUpperCase().match('T')) {
				return new modules.ISODate(arr[0], format);
			}else{
				for(i = 0; i < arr.length; i++) {
				value = arr[i];
	  
				// date pattern
				if( value.charAt(4) === '-' && out.hasFullDate() === false ){
					out.parseDate(value);
				}
				
				// time pattern
				if( (value.indexOf(':') > -1 || modules.utils.isNumber( this.parseAmPmTime(value) )) && out.hasTime() === false ) {
					// split time and timezone
					var items = this.splitTimeAndZone(value);
					value = items[0];
					
					// parse any use of am/pm
					value = this.parseAmPmTime(value);
					out.parseTime(value);
					
					// parse any timezone 
					if(items.length > 1){
						 out.parseTimeZone(items[1], format);
					}
				}
				
				// timezone pattern
				if(value.charAt(0) === '-' || value.charAt(0) === '+' || value.toUpperCase() === 'Z') {
					if( out.hasTimeZone() === false ){
						out.parseTimeZone(value);
					}
				}
	
			}
			return out;
				
			}
		},
		
		
	   /**
		 * parses text by splitting it into an array of time and timezone strings
		 *
		 * @param  {String} text
		 * @return {Array} Modules.ISODate
		 */ 
		splitTimeAndZone: function ( text ){
		   var out = [text],
			   chars = ['-','+','z','Z'],
			   i = chars.length;
			   
			while (i--) {
			  if(text.indexOf(chars[i]) > -1){
				  out[0] = text.slice( 0, text.indexOf(chars[i]) );
				  out.push( text.slice( text.indexOf(chars[i]) ) );
				  break;
			   }
			}
		   return out;
		}
		
	};


	modules.text = {
		
		// normalised or whitespace or whitespacetrimmed
		textFormat: 'whitespacetrimmed', 
		
		// block level tags, used to add line returns
		blockLevelTags: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'hr', 'pre', 'table',
			'address', 'article', 'aside', 'blockquote', 'caption', 'col', 'colgroup', 'dd', 'div', 
			'dt', 'dir', 'fieldset', 'figcaption', 'figure', 'footer', 'form',  'header', 'hgroup', 'hr', 
			'li', 'map', 'menu', 'nav', 'optgroup', 'option', 'section', 'tbody', 'testarea', 
			'tfoot', 'th', 'thead', 'tr', 'td', 'ul', 'ol', 'dl', 'details'],

		// tags to exclude 
		excludeTags: ['noframe', 'noscript', 'template', 'script', 'style', 'frames', 'frameset'],
 
	
		/**
		 * parses the text from the DOM Node 
		 *
		 * @param  {DOM Node} node
		 * @param  {String} textFormat
		 * @return {String}
		 */
		parse: function(doc, node, textFormat){
			var out;
			this.textFormat = (textFormat)? textFormat : this.textFormat;
			if(this.textFormat === 'normalised'){
				out = this.walkTreeForText( node );
				if(out !== undefined){
					return this.normalise( doc, out );
				}else{
					return '';
				}
			}else{
			   return this.formatText( doc, modules.domUtils.textContent(node), this.textFormat );
			}
		},
		
		
		/**
		 * parses the text from a html string 
		 *
		 * @param  {DOM Document} doc
		 * @param  {String} text
		 * @param  {String} textFormat
		 * @return {String}
		 */  
		parseText: function( doc, text, textFormat ){
		   var node = modules.domUtils.createNodeWithText( 'div', text );
		   return this.parse( doc, node, textFormat );
		},
		
		
		/**
		 * parses the text from a html string - only for whitespace or whitespacetrimmed formats
		 *
		 * @param  {String} text
		 * @param  {String} textFormat
		 * @return {String}
		 */  
		formatText: function( doc, text, textFormat ){
		   this.textFormat = (textFormat)? textFormat : this.textFormat;
		   if(text){
			  var out = '',
				  regex = /(<([^>]+)>)/ig;
				
			  out = text.replace(regex, '');   
			  if(this.textFormat === 'whitespacetrimmed') {    
				 out = modules.utils.trimWhitespace( out );
			  }
			  
			  //return entities.decode( out, 2 );
			  return modules.domUtils.decodeEntities( doc, out );
		   }else{
			  return ''; 
		   }
		},
		
		
		/**
		 * normalises whitespace in given text 
		 *
		 * @param  {String} text
		 * @return {String}
		 */ 
		normalise: function( doc, text ){
			text = text.replace( /&nbsp;/g, ' ') ;    // exchanges html entity for space into space char
			text = modules.utils.collapseWhiteSpace( text );     // removes linefeeds, tabs and addtional spaces
			text = modules.domUtils.decodeEntities( doc, text );  // decode HTML entities
			text = text.replace( '–', '-' );          // correct dash decoding
			return modules.utils.trim( text );
		},
		
	 
		/**
		 * walks DOM tree parsing the text from DOM Nodes
		 *
		 * @param  {DOM Node} node
		 * @return {String}
		 */ 
		walkTreeForText: function( node ) {
			var out = '',
				j = 0;
	
			if(node.tagName && this.excludeTags.indexOf( node.tagName.toLowerCase() ) > -1){
				return out;
			}
	
			// if node is a text node get its text
			if(node.nodeType && node.nodeType === 3){
				out += modules.domUtils.getElementText( node ); 
			}
	
			// get the text of the child nodes
			if(node.childNodes && node.childNodes.length > 0){
				for (j = 0; j < node.childNodes.length; j++) {
					var text = this.walkTreeForText( node.childNodes[j] );
					if(text !== undefined){
						out += text;
					}
				}
			}
	
			// if it's a block level tag add an additional space at the end
			if(node.tagName && this.blockLevelTags.indexOf( node.tagName.toLowerCase() ) !== -1){
				out += ' ';
			} 
			
			return (out === '')? undefined : out ;
		}
		
	};


	modules.html = {
		
		// elements which are self-closing
		selfClosingElt: ['area', 'base', 'br', 'col', 'hr', 'img', 'input', 'link', 'meta', 'param', 'command', 'keygen', 'source'],
	

		/**
		 * parse the html string from DOM Node
		 *
		 * @param  {DOM Node} node
		 * @return {String}
		 */ 
		parse: function( node ){
			var out = '',
				j = 0;
	
			// we do not want the outer container
			if(node.childNodes && node.childNodes.length > 0){
				for (j = 0; j < node.childNodes.length; j++) {
					var text = this.walkTreeForHtml( node.childNodes[j] );
					if(text !== undefined){
						out += text;
					}
				}
			}
	
			return out;
		},
	
  
		/**
		 * walks the DOM tree parsing the html string from the nodes
		 *
		 * @param  {DOM Document} doc
		 * @param  {DOM Node} node
		 * @return {String}
		 */ 
		walkTreeForHtml: function( node ) {
			var out = '',
				j = 0;
	
			// if node is a text node get its text
			if(node.nodeType && node.nodeType === 3){
				out += modules.domUtils.getElementText( node ); 
			}
	
		
			// exclude text which has been added with include pattern  - 
			if(node.nodeType && node.nodeType === 1 && modules.domUtils.hasAttribute(node, 'data-include') === false){
	
				// begin tag
				out += '<' + node.tagName.toLowerCase();  
	
				// add attributes
				var attrs = modules.domUtils.getOrderedAttributes(node);
				for (j = 0; j < attrs.length; j++) {
					out += ' ' + attrs[j].name +  '=' + '"' + attrs[j].value + '"';
				}
	
				if(this.selfClosingElt.indexOf(node.tagName.toLowerCase()) === -1){
					out += '>';
				}
	
				// get the text of the child nodes
				if(node.childNodes && node.childNodes.length > 0){
					
					for (j = 0; j < node.childNodes.length; j++) {
						var text = this.walkTreeForHtml( node.childNodes[j] );
						if(text !== undefined){
							out += text;
						}
					}
				}
	
				// end tag
				if(this.selfClosingElt.indexOf(node.tagName.toLowerCase()) > -1){
					out += ' />'; 
				}else{
					out += '</' + node.tagName.toLowerCase() + '>'; 
				}
			} 
			
			return (out === '')? undefined : out;
		}    
	
	
	};


	modules.maps['h-adr'] = {
		root: 'adr',
		name: 'h-adr',
		properties: {
			'post-office-box': {},
			'street-address': {},
			'extended-address': {},
			'locality': {},
			'region': {},
			'postal-code': {},
			'country-name': {}
		}
	};


	modules.maps['h-card'] =  {
		root: 'vcard',
		name: 'h-card',
		properties: {
			'fn': {
				'map': 'p-name'
			},
			'adr': {
				'map': 'p-adr',
				'uf': ['h-adr']
			},
			'agent': {
				'uf': ['h-card']
			},
			'bday': {
				'map': 'dt-bday'
			},
			'class': {},
			'category': {
				'map': 'p-category',
				'relAlt': ['tag']
			},
			'email': {
				'map': 'u-email'
			},
			'geo': {
				'map': 'p-geo', 
				'uf': ['h-geo']
			},
			'key': {
				'map': 'u-key'
			},
			'label': {},
			'logo': {
				'map': 'u-logo'
			},
			'mailer': {},
			'honorific-prefix': {},
			'given-name': {},
			'additional-name': {},
			'family-name': {},
			'honorific-suffix': {},
			'nickname': {},
			'note': {}, // could be html i.e. e-note
			'org': {},
			'p-organization-name': {},
			'p-organization-unit': {},
			'photo': {
				'map': 'u-photo'
			},
			'rev': {
				'map': 'dt-rev'
			},
			'role': {},
			'sequence': {},
			'sort-string': {},
			'sound': {
				'map': 'u-sound'
			},
			'title': {
				'map': 'p-job-title'
			},
			'tel': {},
			'tz': {},
			'uid': {
				'map': 'u-uid'
			},
			'url': {
				'map': 'u-url'
			}
		}
	};


	modules.maps['h-entry'] = {
		root: 'hentry',
		name: 'h-entry',
		properties: {
			'entry-title': {
				'map': 'p-name'
			},
			'entry-summary': {
				'map': 'p-summary'
			},
			'entry-content': {
				'map': 'e-content'
			},
			'published': {
				'map': 'dt-published'
			},
			'updated': {
				'map': 'dt-updated'
			},
			'author': { 
				'uf': ['h-card']
			},
			'category': {
				'map': 'p-category',
				'relAlt': ['tag']
			},
			'geo': {
				'map': 'p-geo', 
				'uf': ['h-geo']
			},
			'latitude': {},
			'longitude': {},
			'url': {
				'map': 'u-url',
				'relAlt': ['bookmark']
			}
		}
	};


	modules.maps['h-event'] = {  
		root: 'vevent',
		name: 'h-event',
		properties: {
			'summary': {
				'map': 'p-name'
			},
			'dtstart': {
				'map': 'dt-start'
			},
			'dtend': {
				'map': 'dt-end'
			},
			'description': {},
			'url': {
				'map': 'u-url'
			},
			'category': {
				'map': 'p-category',
				'relAlt': ['tag']
			},
			'location': {
				'uf': ['h-card']
			},
			'geo': {
				'uf': ['h-geo']
			},
			'latitude': {},
			'longitude': {},
			'duration': {
				'map': 'dt-duration'
			},
			'contact': {
				'uf': ['h-card']
			},
			'organizer': {
				'uf': ['h-card']},
			'attendee': {
				'uf': ['h-card']},
			'uid': {
				'map': 'u-uid'
			},
			'attach': {
				'map': 'u-attach'
			},
			'status': {},
			'rdate': {}, 
			'rrule': {}
		}
	};


	modules.maps['h-feed'] = {
		root: 'hfeed',
		name: 'h-feed',
		properties: {
			'category': {
				'map': 'p-category',
				'relAlt': ['tag']
			},
			'summary': {
				'map': 'p-summary'
			},
			'author': { 
				'uf': ['h-card']
			},
			'url': {
				'map': 'u-url'
			},
			'photo': {
				'map': 'u-photo'
			},
		}
	};


	modules.maps['h-geo'] = {
		root: 'geo',
		name: 'h-geo',
		properties: {
			'latitude': {},
			'longitude': {}
		}
	};


	modules.maps['h-item'] = {
		root: 'item',
		name: 'h-item',
		subTree: false,
		properties: {
			'fn': {
				'map': 'p-name'
			},
			'url': {
				'map': 'u-url'
			},
			'photo': {
				'map': 'u-photo'
			}
		}
	};


	modules.maps['h-listing'] = {
			root: 'hlisting',
			name: 'h-listing',
			properties: {
				'version': {},
				'lister': {
					'uf': ['h-card']
				},
				'dtlisted': {
					'map': 'dt-listed'
				},
				'dtexpired': {
					'map': 'dt-expired'
				},
				'location': {},
				'price': {},
				'item': {
					'uf': ['h-card','a-adr','h-geo']
				},
				'summary': {
					'map': 'p-name'
				},
				'description': {
					'map': 'e-description'
				},
				'listing': {}
			}
		};


	modules.maps['h-news'] = {
			root: 'hnews',
			name: 'h-news',
			properties: {
				'entry': {
					'uf': ['h-entry']
				},
				'geo': {
					'uf': ['h-geo']
				},
				'latitude': {},
				'longitude': {},
				'source-org': {
					'uf': ['h-card']
				},
				'dateline': {
					'uf': ['h-card']
				},
				'item-license': {
					'map': 'u-item-license'
				},
				'principles': {
					'map': 'u-principles', 
					'relAlt': ['principles']
				}
			}
		};


	modules.maps['h-org'] = {
		root: 'h-x-org',  // drop this from v1 as it causes issue with fn org hcard pattern
		name: 'h-org',
		childStructure: true,
		properties: {
			'organization-name': {},
			'organization-unit': {}
		}
	};


	modules.maps['h-product'] = {
			root: 'hproduct',
			name: 'h-product',
			properties: {
				'brand': {
					'uf': ['h-card']
				},
				'category': {
					'map': 'p-category',
					'relAlt': ['tag']
				},
				'price': {},
				'description': {
					'map': 'e-description'
				},
				'fn': {
					'map': 'p-name'
				},
				'photo': {
					'map': 'u-photo'
				},
				'url': {
					'map': 'u-url'
				},
				'review': {
					'uf': ['h-review', 'h-review-aggregate']
				},
				'listing': {
					'uf': ['h-listing']
				},
				'identifier': {
					'map': 'u-identifier'
				}
			}
		};


	modules.maps['h-recipe'] = {
			root: 'hrecipe',
			name: 'h-recipe',
			properties: {
				'fn': {
					'map': 'p-name'
				},
				'ingredient': {
					'map': 'e-ingredient'
				},
				'yield': {},
				'instructions': {
					'map': 'e-instructions'
				},
				'duration': {
					'map': 'dt-duration'
				},
				'photo': {
					'map': 'u-photo'
				},
				'summary': {},
				'author': {
					'uf': ['h-card']
				},
				'published': {
					'map': 'dt-published'
				},
				'nutrition': {},
				'category': {
					'map': 'p-category',
					'relAlt': ['tag']
				},
			}
		};


	modules.maps['h-resume'] = {
		root: 'hresume',
		name: 'h-resume',
		properties: {
			'summary': {},
			'contact': {
				'uf': ['h-card']
			},
			'education': {
				'uf': ['h-card', 'h-event']
			},
			'experience': {
				'uf': ['h-card', 'h-event']
			},
			'skill': {},
			'affiliation': {
				'uf': ['h-card']
			}
		}
	};


	modules.maps['h-review-aggregate'] = {
		root: 'hreview-aggregate',
		name: 'h-review-aggregate',
		properties: {
			'summary': {
				'map': 'p-name'
			},
			'item': {
				'map': 'p-item',
				'uf': ['h-item', 'h-geo', 'h-adr', 'h-card', 'h-event', 'h-product']
			},
			'rating': {},
			'average': {},
			'best': {},
			'worst': {},       
			'count': {},
			'votes': {},
			'category': {
				'map': 'p-category',
				'relAlt': ['tag']
			},
			'url': {
				'map': 'u-url',
				'relAlt': ['self', 'bookmark']
			}
		}
	};


	modules.maps['h-review'] = {
		root: 'hreview',
		name: 'h-review',
		properties: {
			'summary': {
				'map': 'p-name'
			},
			'description': {
				'map': 'e-description'
			},
			'item': {
				'map': 'p-item',
				'uf': ['h-item', 'h-geo', 'h-adr', 'h-card', 'h-event', 'h-product']
			},
			'reviewer': {
				'uf': ['h-card']
			},
			'dtreviewer': {
				'map': 'dt-reviewer'
			},
			'rating': {},
			'best': {},
			'worst': {},
			'category': {
				'map': 'p-category',
				'relAlt': ['tag']
			},
			'url': {
				'map': 'u-url',
				'relAlt': ['self', 'bookmark']
			}
		}
	};


	modules.rels = {
		// xfn
		'friend': [ 'yes','external'], 
		'acquaintance': [ 'yes','external'],  
		'contact': [ 'yes','external'], 
		'met': [ 'yes','external'], 
		'co-worker': [ 'yes','external'],  
		'colleague': [ 'yes','external'], 
		'co-resident': [ 'yes','external'],  
		'neighbor': [ 'yes','external'], 
		'child': [ 'yes','external'],  
		'parent': [ 'yes','external'],  
		'sibling': [ 'yes','external'],  
		'spouse': [ 'yes','external'],  
		'kin': [ 'yes','external'], 
		'muse': [ 'yes','external'],  
		'crush': [ 'yes','external'],  
		'date': [ 'yes','external'],  
		'sweetheart': [ 'yes','external'], 
		'me': [ 'yes','external'], 
	
		// other rel=* 
		'license': [ 'yes','yes'],
		'nofollow': [ 'no','external'],
		'tag': [ 'no','yes'],
		'self': [ 'no','external'],
		'bookmark': [ 'no','external'],
		'author': [ 'no','external'],
		'home': [ 'no','external'],
		'directory': [ 'no','external'],
		'enclosure': [ 'no','external'],
		'pronunciation': [ 'no','external'],
		'payment': [ 'no','external'],
		'principles': [ 'no','external']
	
	};



    var External = {
        version: modules.version,
        livingStandard: modules.livingStandard
    };
    
    
    External.get = function(options){
    	var parser = new modules.Parser();
        addV1(parser, options);
    	return parser.get( options );
    };
    
    
    External.getParent = function(node, options){
    	var parser = new modules.Parser();
        addV1(parser, options);
    	return parser.getParent( node, options );
    };
    
    
    External.count = function(options){
    	var parser = new modules.Parser();
        addV1(parser, options);
    	return parser.count( options );
    };
    
    
    External.isMicroformat = function( node, options ){
    	var parser = new modules.Parser();
        addV1(parser, options);
    	return parser.isMicroformat( node, options );
    };
    
    
    External.hasMicroformats = function( node, options ){
    	var parser = new modules.Parser();
        addV1(parser, options);
    	return parser.hasMicroformats( node, options );
    };
    
    
    function addV1(parser, options){
		if(options && options.maps){
			if(Array.isArray(options.maps)){
				parser.add(options.maps);
			}else{
				parser.add([options.maps]);
			}
		}
    }
    
    
    return External;
    
    
}));

// Based on https://gist.github.com/1129031 By Eli Grey, http://eligrey.com - Public domain.

// DO NOT use https://developer.mozilla.org/en-US/docs/Web/API/DOMParser example polyfill
// as it does not work with earlier versions of Chrome


(function(DOMParser) {var DOMParser_proto;
    var real_parseFromString;
    var textHTML;         // Flag for text/html support
    var textXML;          // Flag for text/xml support
    var htmlElInnerHTML;  // Flag for support for setting html element's innerHTML

    // Stop here if DOMParser not defined
    if (!DOMParser) {
        return;
    }

    // Firefox, Opera and IE throw errors on unsupported types
    try {
        // WebKit returns null on unsupported types
        textHTML = !!(new DOMParser()).parseFromString('', 'text/html');

    } catch (er) {
      textHTML = false;
    }

    // If text/html supported, don't need to do anything.
    if (textHTML) {
        return;
    }

    // Next try setting innerHTML of a created document
    // IE 9 and lower will throw an error (can't set innerHTML of its HTML element)
    try {
      var doc = document.implementation.createHTMLDocument('');
      doc.documentElement.innerHTML = '<title></title><div></div>';
      htmlElInnerHTML = true;

    } catch (er) {
      htmlElInnerHTML = false;
    }

    // If if that failed, try text/xml
    if (!htmlElInnerHTML) {

        try {
            textXML = !!(new DOMParser()).parseFromString('', 'text/xml');

        } catch (er) {
            textHTML = false;
        }
    }

    // Mess with DOMParser.prototype (less than optimal...) if one of the above worked
    // Assume can write to the prototype, if not, make this a stand alone function
    if (DOMParser.prototype && (htmlElInnerHTML || textXML)) {
        DOMParser_proto = DOMParser.prototype;
        real_parseFromString = DOMParser_proto.parseFromString;

        DOMParser_proto.parseFromString = function (markup, type) {

            // Only do this if type is text/html
            if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
                var doc, doc_el, first_el;

                // Use innerHTML if supported
                if (htmlElInnerHTML) {
                    doc = document.implementation.createHTMLDocument('');
                    doc_el = doc.documentElement;
                    doc_el.innerHTML = markup;
                    first_el = doc_el.firstElementChild;

                // Otherwise use XML method
                } else if (textXML) {

                    // Make sure markup is wrapped in HTML tags
                    // Should probably allow for a DOCTYPE
                    if (!(/^<html.*html>$/i.test(markup))) {
                        markup = '<html>' + markup + '<\/html>';
                    }
                    doc = (new DOMParser()).parseFromString(markup, 'text/xml');
                    doc_el = doc.documentElement;
                    first_el = doc_el.firstElementChild;
                }

                // Is this an entire document or a fragment?
                if (doc_el.childElementCount === 1 && first_el.localName.toLowerCase() === 'html') {
                    doc.replaceChild(first_el, doc_el);
                }

                return doc;

            // If not text/html, send as-is to host method
            } else {
                return real_parseFromString.apply(this, arguments);
            }
        };
    }
}(DOMParser));