code:
<?php

namespace Mf2;

use DOMDocument;
use DOMElement;
use DOMXPath;
use DOMNode;
use DOMNodeList;
use Exception;
use SplObjectStorage;
use stdClass;

/**
 * Parse Microformats2
 *
 * Functional shortcut for the commonest cases of parsing microformats2 from HTML.
 *
 * Example usage:
 *
 *     use Mf2;
 *     $output = Mf2\parse('Barnaby Walters');
 *     echo json_encode($output, JSON_PRETTY_PRINT);
 *
 * Produces:
 *
 *     {
 *      "items": [
 *       {
 *        "type": ["h-card"],
 *        "properties": {
 *         "name": ["Barnaby Walters"]
 *        }
 *       }
 *      ],
 *      "rels": {}
 *     }
 *
 * @param string|DOMDocument $input The HTML string or DOMDocument object to parse
 * @param string $url The URL the input document was found at, for relative URL resolution
 * @param bool $convertClassic whether or not to convert classic microformats
 * @return array Canonical MF2 array structure
 */
function parse($input, $url = null, $convertClassic = true) {
	$parser = new Parser($input, $url);
	return $parser->parse($convertClassic);
}

/**
 * Fetch microformats2
 *
 * Given a URL, fetches it (following up to 5 redirects) and, if the content-type appears to be HTML, returns the parsed
 * microformats2 array structure.
 *
 * Not that even if the response code was a 4XX or 5XX error, if the content-type is HTML-like then it will be parsed
 * all the same, as there are legitimate cases where error pages might contain useful microformats (for example a deleted
 * h-entry resulting in a 410 Gone page with a stub h-entry explaining the reason for deletion). Look in $curlInfo['http_code']
 * for the actual value.
 *
 * @param string $url The URL to fetch
 * @param bool $convertClassic (optional, default true) whether or not to convert classic microformats
 * @param &array $curlInfo (optional) the results of curl_getinfo will be placed in this variable for debugging
 * @return array|null canonical microformats2 array structure on success, null on failure
 */
function fetch($url, $convertClassic = true, &$curlInfo=null) {
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_HEADER, 0);
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
	curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
	curl_setopt($ch, CURLOPT_HTTPHEADER, array(
		'Accept: text/html'
	));
	$html = curl_exec($ch);
	$info = $curlInfo = curl_getinfo($ch);
	curl_close($ch);

	if (strpos(strtolower($info['content_type']), 'html') === false) {
		// The content was not delivered as HTML, do not attempt to parse it.
		return null;
	}

	# ensure the final URL is used to resolve relative URLs
	$url = $info['url'];

	return parse($html, $url, $convertClassic);
}

/**
 * Unicode to HTML Entities
 * @param string $input String containing characters to convert into HTML entities
 * @return string
 */
function unicodeToHtmlEntities($input) {
	return mb_convert_encoding($input, 'HTML-ENTITIES', mb_detect_encoding($input));
}

/**
 * Collapse Whitespace
 *
 * Collapses any sequences of whitespace within a string into a single space
 * character.
 *
 * @deprecated since v0.2.3
 * @param string $str
 * @return string
 */
function collapseWhitespace($str) {
	return preg_replace('/[\s|\n]+/', ' ', $str);
}

function unicodeTrim($str) {
	// this is cheating. TODO: find a better way if this causes any problems
	$str = str_replace(mb_convert_encoding(' ', 'UTF-8', 'HTML-ENTITIES'), ' ', $str);
	$str = preg_replace('/^\s+/', '', $str);
	return preg_replace('/\s+$/', '', $str);
}

/**
 * Microformat Name From Class string
 *
 * Given the value of @class, get the relevant mf classnames (e.g. h-card,
 * p-name).
 *
 * @param string $class A space delimited list of classnames
 * @param string $prefix The prefix to look for
 * @return string|array The prefixed name of the first microfomats class found or false
 */
function mfNamesFromClass($class, $prefix='h-') {
	$class = str_replace(array(' ', '	', "\n"), ' ', $class);
	$classes = explode(' ', $class);
	$classes = preg_grep('#^(h|p|u|dt|e)-([a-z0-9]+-)?[a-z]+(-[a-z]+)*$#', $classes);
	$matches = array();

	foreach ($classes as $classname) {
		$compare_classname = ' ' . $classname;
		$compare_prefix = ' ' . $prefix;
		if (strstr($compare_classname, $compare_prefix) !== false && ($compare_classname != $compare_prefix)) {
			$matches[] = ($prefix === 'h-') ? $classname : substr($classname, strlen($prefix));
		}
	}

	return $matches;
}

/**
 * Get Nested µf Property Name From Class
 *
 * Returns all the p-, u-, dt- or e- prefixed classnames it finds in a
 * space-separated string.
 *
 * @param string $class
 * @return array
 */
function nestedMfPropertyNamesFromClass($class) {
	$prefixes = array('p-', 'u-', 'dt-', 'e-');
	$propertyNames = array();

	$class = str_replace(array(' ', '	', "\n"), ' ', $class);
	foreach (explode(' ', $class) as $classname) {
		foreach ($prefixes as $prefix) {
			// Check if $classname is a valid property classname for $prefix.
			if (mb_substr($classname, 0, mb_strlen($prefix)) == $prefix && $classname != $prefix) {
				$propertyName = mb_substr($classname, mb_strlen($prefix));
				$propertyNames[$propertyName][] = $prefix;
			}
		}
	}

	foreach ($propertyNames as $property => $prefixes) {
		$propertyNames[$property] = array_unique($prefixes);
	}

	return $propertyNames;
}

/**
 * Wraps mfNamesFromClass to handle an element as input (common)
 *
 * @param DOMElement $e The element to get the classname for
 * @param string $prefix The prefix to look for
 * @return mixed See return value of mf2\Parser::mfNameFromClass()
 */
function mfNamesFromElement(\DOMElement $e, $prefix = 'h-') {
	$class = $e->getAttribute('class');
	return mfNamesFromClass($class, $prefix);
}

/**
 * Wraps nestedMfPropertyNamesFromClass to handle an element as input
 */
function nestedMfPropertyNamesFromElement(\DOMElement $e) {
	$class = $e->getAttribute('class');
	return nestedMfPropertyNamesFromClass($class);
}

/**
 * Converts various time formats to HH:MM
 * @param string $time The time to convert
 * @return string
 */
function convertTimeFormat($time) {
	$hh = $mm = $ss = '';
	preg_match('/(\d{1,2}):?(\d{2})?:?(\d{2})?(a\.?m\.?|p\.?m\.?)?/i', $time, $matches);

	// If no am/pm is specified:
	if (empty($matches[4])) {
		return $time;
	} else {
		// Otherwise, am/pm is specified.
		$meridiem = strtolower(str_replace('.', '', $matches[4]));

		// Hours.
		$hh = $matches[1];

		// Add 12 to hours if pm applies.
		if ($meridiem == 'pm' && ($hh < 12)) {
			$hh += 12;
		}

		$hh = str_pad($hh, 2, '0', STR_PAD_LEFT);

		// Minutes.
		$mm = (empty($matches[2]) ) ? '00' : $matches[2];

		// Seconds, only if supplied.
		if (!empty($matches[3])) {
			$ss = $matches[3];
		}

		if (empty($ss)) {
			return sprintf('%s:%s', $hh, $mm);
		}
		else {
			return sprintf('%s:%s:%s', $hh, $mm, $ss);
		}
	}
}

/**
 * Normalize an ordinal date to YYYY-MM-DD
 * This function should only be called after validating the $dtValue
 * matches regex \d{4}-\d{2}
 * @param string $dtValue
 * @return string
 */
function normalizeOrdinalDate($dtValue) {
	list($year, $day) = explode('-', $dtValue, 2);
	$day = intval($day);
	if ($day < 367 && $day > 0) {
		$date = \DateTime::createFromFormat('Y-z', $dtValue);
		$date->modify('-1 day'); # 'z' format is zero-based so need to adjust
		if ($date->format('Y') === $year) {
			return $date->format('Y-m-d');
		}
	}
	return '';
}

/**
 * If a date value has a timezone offset, normalize it.
 * @param string $dtValue
 * @return string isolated, normalized TZ offset for implied TZ for other dt- properties
 */
function normalizeTimezoneOffset(&$dtValue) {
	preg_match('/Z|[+-]\d{1,2}:?(\d{2})?$/i', $dtValue, $matches);

	if (empty($matches)) {
		return null;
	}

	$timezoneOffset = null;

	if ( $matches[0] != 'Z' ) {
		$timezoneString = str_replace(':', '', $matches[0]);
		$plus_minus = substr($timezoneString, 0, 1);
		$timezoneOffset = substr($timezoneString, 1);
		if ( strlen($timezoneOffset) <= 2 ) {
			$timezoneOffset .= '00';
		}
		$timezoneOffset = str_pad($timezoneOffset, 4, 0, STR_PAD_LEFT);
		$timezoneOffset = $plus_minus . $timezoneOffset;
		$dtValue = preg_replace('/Z?[+-]\d{1,2}:?(\d{2})?$/i', $timezoneOffset, $dtValue);
	}

	return $timezoneOffset;
}

function applySrcsetUrlTransformation($srcset, $transformation) {
	return implode(', ', array_filter(array_map(function ($srcsetPart) use ($transformation) {
		$parts = explode(" \t\n\r\0\x0B", trim($srcsetPart), 2);
		$parts[0] = rtrim($parts[0]);

		if (empty($parts[0])) { return false; }

		$parts[0] = call_user_func($transformation, $parts[0]);

		return $parts[0] . (empty($parts[1]) ? '' : ' ' . $parts[1]);
	}, explode(',', trim($srcset)))));
}

/**
 * Microformats2 Parser
 *
 * A class which holds state for parsing microformats2 from HTML.
 *
 * Example usage:
 *
 *     use Mf2;
 *     $parser = new Mf2\Parser('
Barnaby Walters
');
 *     $output = $parser->parse();
 */
class Parser {
	/** @var string The baseurl (if any) to use for this parse */
	public $baseurl;

	/** @var DOMXPath object which can be used to query over any fragment*/
	public $xpath;

	/** @var DOMDocument */
	public $doc;

	/** @var SplObjectStorage */
	protected $parsed;

	/**
	 * @var bool
	 */
	public $jsonMode;

	/** @var boolean Whether to include experimental language parsing in the result */
	public $lang = false;

	/** @var bool Whether to include alternates object (dropped from spec in favor of rel-urls) */
	public $enableAlternates = false;

	/**
	 * Elements upgraded to mf2 during backcompat
	 * @var SplObjectStorage
	 */
	protected $upgraded;

	/**
	 * Whether to convert classic microformats
	 * @var bool
	 */
	public $convertClassic;

	/**
	 * Constructor
	 *
	 * @param DOMDocument|string $input The data to parse. A string of HTML or a DOMDocument
	 * @param string $url The URL of the parsed document, for relative URL resolution
	 * @param boolean $jsonMode Whether or not to use a stdClass instance for an empty `rels` dictionary. This breaks PHP looping over rels, but allows the output to be correctly serialized as JSON.
	 */
	public function __construct($input, $url = null, $jsonMode = false) {
		libxml_use_internal_errors(true);
		if (is_string($input)) {
			if (class_exists('Masterminds\\HTML5')) {
					$doc = new \Masterminds\HTML5(array('disable_html_ns' => true));
					$doc = $doc->loadHTML($input);
			} else {
				$doc = new DOMDocument();
				@$doc->loadHTML(unicodeToHtmlEntities($input));
			}
		} elseif (is_a($input, 'DOMDocument')) {
			$doc = clone $input;
		} else {
			$doc = new DOMDocument();
			@$doc->loadHTML('');
		}

		$this->xpath = new DOMXPath($doc);

		$baseurl = $url;
		foreach ($this->xpath->query('//base[@href]') as $base) {
			$baseElementUrl = $base->getAttribute('href');

			if (parse_url($baseElementUrl, PHP_URL_SCHEME) === null) {
				/* The base element URL is relative to the document URL.
				 *
				 * :/
				 *
				 * Perhaps the author was high? */

				$baseurl = resolveUrl($url, $baseElementUrl);
			} else {
				$baseurl = $baseElementUrl;
			}
			break;
		}

		// Ignore <template> elements as per the HTML5 spec
		foreach ($this->xpath->query('//template') as $templateEl) {
			$templateEl->parentNode->removeChild($templateEl);
		}

		$this->baseurl = $baseurl;
		$this->doc = $doc;
		$this->parsed = new SplObjectStorage();
		$this->upgraded = new SplObjectStorage();
		$this->jsonMode = $jsonMode;
	}

	private function elementPrefixParsed(\DOMElement $e, $prefix) {
		if (!$this->parsed->contains($e))
			$this->parsed->attach($e, array());

		$prefixes = $this->parsed[$e];
		$prefixes[] = $prefix;
		$this->parsed[$e] = $prefixes;
	}

	/**
	 * Determine if the element has already been parsed
	 * @param DOMElement $e
	 * @param string $prefix
	 * @return bool
	 */
	private function isElementParsed(\DOMElement $e, $prefix) {
		if (!$this->parsed->contains($e)) {
			return false;
		}

		$prefixes = $this->parsed[$e];

		if (!in_array($prefix, $prefixes)) {
			return false;
		}

		return true;
	}

	/**
	 * Determine if the element's specified property has already been upgraded during backcompat
	 * @param DOMElement $el
	 * @param string $property
	 * @return bool
	 */
	private function isElementUpgraded(\DOMElement $el, $property) {
		if ( $this->upgraded->contains($el) ) {
			if ( in_array($property, $this->upgraded[$el]) ) {
				return true;
			}
		}

		return false;
	}

	private function resolveChildUrls(DOMElement $el) {
		$hyperlinkChildren = $this->xpath->query('.//*[@src or @href or @data]', $el);

		foreach ($hyperlinkChildren as $child) {
			if ($child->hasAttribute('href'))
				$child->setAttribute('href', $this->resolveUrl($child->getAttribute('href')));
			if ($child->hasAttribute('src'))
				$child->setAttribute('src', $this->resolveUrl($child->getAttribute('src')));
			if ($child->hasAttribute('srcset'))
				$child->setAttribute('srcset', applySrcsetUrlTransformation($child->getAttribute('href'), array($this, 'resolveUrl')));
			if ($child->hasAttribute('data'))
				$child->setAttribute('data', $this->resolveUrl($child->getAttribute('data')));
		}
	}

	/**
	 * The following two methods implements plain text parsing.
	 * @param DOMElement $element
	 * @param bool $implied
	 * @see https://wiki.zegnat.net/media/textparsing.html
	 **/
	public function textContent(DOMElement $element, $implied=false)
	{
				return preg_replace(
						'/(^[\t\n\f\r ]+| +(?=\n)|(?<=\n) +| +(?= )|[\t\n\f\r ]+$)/',
						'',
						$this->elementToString($element, $implied)
				);
	}
	private function elementToString(DOMElement $input, $implied=false)
	{
			$output = '';
			foreach ($input->childNodes as $child) {
					if ($child->nodeType === XML_TEXT_NODE) {
							$output .= str_replace(array("\t", "\n", "\r") , ' ', $child->textContent);
					} else if ($child->nodeType === XML_ELEMENT_NODE) {
							$tagName = strtoupper($child->tagName);
							if (in_array($tagName, array('SCRIPT', 'STYLE'))) {
									continue;
							} else if ($tagName === 'IMG') {
									if ($child->hasAttribute('alt')) {
											$output .= ' ' . trim($child->getAttribute('alt'), "\t\n\f\r ") . ' ';
									} else if (!$implied && $child->hasAttribute('src')) {
											$output .= ' ' . $this->resolveUrl(trim($child->getAttribute('src'), "\t\n\f\r ")) . ' ';
									}
							} else if ($tagName === 'BR') {
									$output .= "\n";
							} else if ($tagName === 'P') {
									$output .= "\n" . $this->elementToString($child);
							} else {
									$output .= $this->elementToString($child);
							}
					}
			}
			return $output;
	}

	/**
	 * This method parses the language of an element
	 * @param DOMElement $el
	 * @access public
	 * @return string
	 */
	public function language(DOMElement $el)
	{
		// element has a lang attribute; use it
		if ($el->hasAttribute('lang')) {
			return unicodeTrim($el->getAttribute('lang'));
		}

		if ($el->tagName == 'html') {
			// we're at the  element and no lang; check <meta> http-equiv Content-Language
			foreach ( $this->xpath->query('.//meta[@http-equiv]') as $node )
			{
				if ($node->hasAttribute('http-equiv') && $node->hasAttribute('content') && strtolower($node->getAttribute('http-equiv')) == 'content-language') {
					return unicodeTrim($node->getAttribute('content'));
				}
			}
		} elseif ($el->parentNode instanceof DOMElement) {
			// check the parent node
			return $this->language($el->parentNode);
		}

		return '';
	} # end method language()

	// TODO: figure out if this has problems with sms: and geo: URLs
	public function resolveUrl($url) {
		// If the URL is seriously malformed it’s probably beyond the scope of this
		// parser to try to do anything with it.
		if (parse_url($url) === false) {
			return $url;
		}

		// per issue #40 valid URLs could have a space on either side
		$url = trim($url);

		$scheme = parse_url($url, PHP_URL_SCHEME);

		if (empty($scheme) and !empty($this->baseurl)) {
			return resolveUrl($this->baseurl, $url);
		} else {
			return $url;
		}
	}

	// Parsing Functions

	/**
	 * Parse value-class/value-title on an element, joining with $separator if
	 * there are multiple.
	 *
	 * @param \DOMElement $e
	 * @param string $separator = '' if multiple value-title elements, join with this string
	 * @return string|null the parsed value or null if value-class or -title aren’t in use
	 */
	public function parseValueClassTitle(\DOMElement $e, $separator = '') {
		$valueClassElements = $this->xpath->query('./*[contains(concat(" ", @class, " "), " value ")]', $e);

		if ($valueClassElements->length !== 0) {
			// Process value-class stuff
			$val = '';
			foreach ($valueClassElements as $el) {
				$val .= $this->textContent($el);
			}

			return unicodeTrim($val);
		}

		$valueTitleElements = $this->xpath->query('./*[contains(concat(" ", @class, " "), " value-title ")]', $e);

		if ($valueTitleElements->length !== 0) {
			// Process value-title stuff
			$val = '';
			foreach ($valueTitleElements as $el) {
				$val .= $el->getAttribute('title');
			}

			return unicodeTrim($val);
		}

		// No value-title or -class in this element
		return null;
	}

	/**
	 * Given an element with class="p-*", get its value
	 *
	 * @param DOMElement $p The element to parse
	 * @return string The plaintext value of $p, dependant on type
	 * @todo Make this adhere to value-class
	 */
	public function parseP(\DOMElement $p) {
		$classTitle = $this->parseValueClassTitle($p, ' ');

		if ($classTitle !== null) {
			return $classTitle;
		}

		$this->resolveChildUrls($p);

		if ($p->tagName == 'img' and $p->hasAttribute('alt')) {
			$pValue = $p->getAttribute('alt');
		} elseif ($p->tagName == 'area' and $p->hasAttribute('alt')) {
			$pValue = $p->getAttribute('alt');
		} elseif (($p->tagName == 'abbr' or $p->tagName == 'link') and $p->hasAttribute('title')) {
			$pValue = $p->getAttribute('title');
		} elseif (in_array($p->tagName, array('data', 'input')) and $p->hasAttribute('value')) {
			$pValue = $p->getAttribute('value');
		} else {
			$pValue = $this->textContent($p);
		}

		return $pValue;
	}

	/**
	 * Given an element with class="u-*", get the value of the URL
	 *
	 * @param DOMElement $u The element to parse
	 * @return string The plaintext value of $u, dependant on type
	 * @todo make this adhere to value-class
	 */
	public function parseU(\DOMElement $u) {
		if (($u->tagName == 'a' or $u->tagName == 'area' or $u->tagName == 'link') and $u->hasAttribute('href')) {
			$uValue = $u->getAttribute('href');
		} elseif (in_array($u->tagName, array('img', 'audio', 'video', 'source')) and $u->hasAttribute('src')) {
			$uValue = $u->getAttribute('src');
		} elseif ($u->tagName == 'video' and !$u->hasAttribute('src') and $u->hasAttribute('poster')) {
			$uValue = $u->getAttribute('poster');
		} elseif ($u->tagName == 'object' and $u->hasAttribute('data')) {
			$uValue = $u->getAttribute('data');
		} elseif (($classTitle = $this->parseValueClassTitle($u)) !== null) {
				$uValue = $classTitle;
		} elseif (($u->tagName == 'abbr' or $u->tagName == 'link') and $u->hasAttribute('title')) {
			$uValue = $u->getAttribute('title');
		} elseif (in_array($u->tagName, array('data', 'input')) and $u->hasAttribute('value')) {
			$uValue = $u->getAttribute('value');
		} else {
			$uValue = $this->textContent($u);
		}
				return $this->resolveUrl($uValue);
	}

	/**
	 * Given an element with class="dt-*", get the value of the datetime as a php date object
	 *
	 * @param DOMElement $dt The element to parse
	 * @param array $dates Array of dates processed so far
	 * @param string $impliedTimezone
	 * @return string The datetime string found
	 */
	public function parseDT(\DOMElement $dt, &$dates = array(), &$impliedTimezone = null) {
		// Check for value-class pattern
		$valueClassChildren = $this->xpath->query('./*[contains(concat(" ", @class, " "), " value ") or contains(concat(" ", @class, " "), " value-title ")]', $dt);
		$dtValue = false;

		if ($valueClassChildren->length > 0) {
			// They’re using value-class
			$dateParts = array();

			foreach ($valueClassChildren as $e) {
				if (strstr(' ' . $e->getAttribute('class') . ' ', ' value-title ')) {
					$title = $e->getAttribute('title');
					if (!empty($title)) {
						$dateParts[] = $title;
					}
				}
				elseif ($e->tagName == 'img' or $e->tagName == 'area') {
					// Use @alt
					$alt = $e->getAttribute('alt');
					if (!empty($alt)) {
						$dateParts[] = $alt;
					}
				}
				elseif ($e->tagName == 'data') {
					// Use @value, otherwise innertext
					$value = $e->hasAttribute('value') ? $e->getAttribute('value') : unicodeTrim($e->nodeValue);
					if (!empty($value)) {
						$dateParts[] = $value;
					}
				}
				elseif ($e->tagName == 'abbr') {
					// Use @title, otherwise innertext
					$title = $e->hasAttribute('title') ? $e->getAttribute('title') : unicodeTrim($e->nodeValue);
					if (!empty($title)) {
						$dateParts[] = $title;
					}
				}
				elseif ($e->tagName == 'del' or $e->tagName == 'ins' or $e->tagName == 'time') {
					// Use @datetime if available, otherwise innertext
					$dtAttr = ($e->hasAttribute('datetime')) ? $e->getAttribute('datetime') : unicodeTrim($e->nodeValue);
					if (!empty($dtAttr)) {
						$dateParts[] = $dtAttr;
					}
				}
				else {
					if (!empty($e->nodeValue)) {
						$dateParts[] = unicodeTrim($e->nodeValue);
					}
				}
			}

			// Look through dateParts
			$datePart = '';
			$timePart = '';
			$timezonePart = '';
			foreach ($dateParts as $part) {
				// Is this part a full ISO8601 datetime?
				if (preg_match('/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?(Z|[+-]\d{2}:?\d{2})?$/', $part)) {
					// Break completely, we’ve got our value.
					$dtValue = $part;
					break;
				} else {
					// Is the current part a valid time(+TZ?) AND no other time representation has been found?
					if ((preg_match('/^\d{1,2}:\d{2}(:\d{2})?(Z|[+-]\d{1,2}:?\d{2})?$/', $part) or preg_match('/^\d{1,2}(:\d{2})?(:\d{2})?[ap]\.?m\.?$/i', $part)) and empty($timePart)) {
						$timePart = $part;

						$timezoneOffset = normalizeTimezoneOffset($timePart);
						if (!$impliedTimezone && $timezoneOffset) {
							$impliedTimezone = $timezoneOffset;
						}
					// Is the current part a valid date AND no other date representation has been found?
					} elseif (preg_match('/^\d{4}-\d{2}-\d{2}$/', $part) and empty($datePart)) {
						$datePart = $part;
					// Is the current part a valid ordinal date AND no other date representation has been found?
					} elseif (preg_match('/^\d{4}-\d{3}$/', $part) and empty($datePart)) {
						$datePart = normalizeOrdinalDate($part);
					// Is the current part a valid timezone offset AND no other timezone part has been found?
					} elseif (preg_match('/^(Z|[+-]\d{1,2}:?(\d{2})?)$/', $part) and empty($timezonePart)) {
						$timezonePart = $part;

						$timezoneOffset = normalizeTimezoneOffset($timezonePart);
						if (!$impliedTimezone && $timezoneOffset) {
							$impliedTimezone = $timezoneOffset;
						}
					// Current part already represented by other VCP parts; do nothing with it
					} else {
						continue;
					}

					if ( !empty($datePart) && !in_array($datePart, $dates) ) {
						$dates[] = $datePart;
					}

					if (!empty($timezonePart) && !empty($timePart)) {
						$timePart .= $timezonePart;
					}

					$dtValue = '';

					if ( empty($datePart) && !empty($timePart) ) {
						$timePart = convertTimeFormat($timePart);
						$dtValue = unicodeTrim($timePart);
					}
					else if ( !empty($datePart) && empty($timePart) ) {
						$dtValue = rtrim($datePart, 'T');
					}
					else {
						$timePart = convertTimeFormat($timePart);
						$dtValue = rtrim($datePart, 'T') . ' ' . unicodeTrim($timePart);
					}
				}
			}
		} else {
			// Not using value-class (phew).
			if ($dt->tagName == 'img' or $dt->tagName == 'area') {
				// Use @alt
				// Is it an entire dt?
				$alt = $dt->getAttribute('alt');
				if (!empty($alt)) {
					$dtValue = $alt;
				}
			} elseif (in_array($dt->tagName, array('data'))) {
				// Use @value, otherwise innertext
				// Is it an entire dt?
				$value = $dt->getAttribute('value');
				if (!empty($value)) {
					$dtValue = $value;
				}
				else {
					$dtValue = $this->textContent($dt);
				}
			} elseif ($dt->tagName == 'abbr') {
				// Use @title, otherwise innertext
				// Is it an entire dt?
				$title = $dt->getAttribute('title');
				if (!empty($title)) {
					$dtValue = $title;
				}
				else {
					$dtValue = $this->textContent($dt);
				}
			} elseif ($dt->tagName == 'del' or $dt->tagName == 'ins' or $dt->tagName == 'time') {
				// Use @datetime if available, otherwise innertext
				// Is it an entire dt?
				$dtAttr = $dt->getAttribute('datetime');
				if (!empty($dtAttr)) {
					$dtValue = $dtAttr;
				}
				else {
					$dtValue = $this->textContent($dt);
				}

			} else {
				$dtValue = $this->textContent($dt);
			}

			// if the dtValue is not just YYYY-MM-DD
			if (!preg_match('/^(\d{4}-\d{2}-\d{2})$/', $dtValue)) {
				// no implied timezone set and dtValue has a TZ offset, use un-normalized TZ offset
				preg_match('/Z|[+-]\d{1,2}:?(\d{2})?$/i', $dtValue, $matches);
				if (!$impliedTimezone && !empty($matches[0])) {
					$impliedTimezone = $matches[0];
				}
			}

			$dtValue = unicodeTrim($dtValue);

			// Store the date part so that we can use it when assembling the final timestamp if the next one is missing a date part
			if (preg_match('/(\d{4}-\d{2}-\d{2})/', $dtValue, $matches)) {
				$dates[] = $matches[0];
			}
		}

		/**
		 * if $dtValue is only a time and there are recently parsed dates,
		 * form the full date-time using the most recently parsed dt- value
		 */
		if ((preg_match('/^\d{1,2}:\d{2}(:\d{2})?(Z|[+-]\d{2}:?\d{2}?)?$/', $dtValue) or preg_match('/^\d{1,2}(:\d{2})?(:\d{2})?[ap]\.?m\.?$/i', $dtValue)) && !empty($dates)) {
			$timezoneOffset = normalizeTimezoneOffset($dtValue);
			if (!$impliedTimezone && $timezoneOffset) {
				$impliedTimezone = $timezoneOffset;
			}

			$dtValue = convertTimeFormat($dtValue);
			$dtValue = end($dates) . ' ' . unicodeTrim($dtValue);
		}

		return $dtValue;
	}

	/**
	 * 	Given the root element of some embedded markup, return a string representing that markup
	 *
	 * 	@param DOMElement $e The element to parse
	 * 	@return string $e’s innerHTML
	 *
	 * @todo need to mark this element as e- parsed so it doesn’t get parsed as it’s parent’s e-* too
	 */
	public function parseE(\DOMElement $e) {
		$classTitle = $this->parseValueClassTitle($e);

		if ($classTitle !== null)
			return $classTitle;

		// Expand relative URLs within children of this element
		// TODO: as it is this is not relative to only children, make this .// and rerun tests
		$this->resolveChildUrls($e);

		// Temporarily move all descendants into a separate DocumentFragment.
		// This way we can DOMDocument::saveHTML on the entire collection at once.
		// Running DOMDocument::saveHTML per node may add whitespace that isn't in source.
		// See https://stackoverflow.com/q/38317903
		$innerNodes = $e->ownerDocument->createDocumentFragment();
		while ($e->hasChildNodes()) {
			$innerNodes->appendChild($e->firstChild);
		}
		$html = $e->ownerDocument->saveHtml($innerNodes);
		// Put the nodes back in place.
		if($innerNodes->hasChildNodes()) {
			$e->appendChild($innerNodes);
		}

		$return = array(
			'html' => unicodeTrim($html),
			'value' => $this->textContent($e),
		);

		if($this->lang) {
			// Language
			if ( $html_lang = $this->language($e) ) {
				$return['lang'] = $html_lang;
			}
		}

		return $return;
	}

	private function removeTags(\DOMElement &$e, $tagName) {
		while(($r = $e->getElementsByTagName($tagName)) && $r->length) {
			$r->item(0)->parentNode->removeChild($r->item(0));
		}
	}

	/**
	 * Recursively parse microformats
	 *
	 * @param DOMElement $e The element to parse
	 * @param bool $is_backcompat Whether using backcompat parsing or not
	 * @param bool $has_nested_mf Whether this microformat has a nested microformat
	 * @return array A representation of the values contained within microformat $e
	 */
	public function parseH(\DOMElement $e, $is_backcompat = false, $has_nested_mf = false) {
		// If it’s already been parsed (e.g. is a child mf), skip
		if ($this->parsed->contains($e)) {
			return null;
		}

		// Get current µf name
		$mfTypes = mfNamesFromElement($e, 'h-');

		if (!$mfTypes) {
			return null;
		}

		// Initalise var to store the representation in
		$return = array();
		$children = array();
		$dates = array();
		$prefixes = array();
		$impliedTimezone = null;

		if($e->tagName == 'area') {
			$coords = $e->getAttribute('coords');
			$shape = $e->getAttribute('shape');
		}

		// Handle p-*
		foreach ($this->xpath->query('.//*[contains(concat(" ", @class) ," p-")]', $e) as $p) {
			// element is already parsed
			if ($this->isElementParsed($p, 'p')) {
				continue;
			// backcompat parsing and element was not upgraded; skip it
			} else if ( $is_backcompat && empty($this->upgraded[$p]) ) {
				$this->elementPrefixParsed($p, 'p');
				continue;
			}

			$prefixes[] = 'p-';
			$pValue = $this->parseP($p);

			// Add the value to the array for it’s p- properties
			foreach (mfNamesFromElement($p, 'p-') as $propName) {
				if (!empty($propName)) {
					$return[$propName][] = $pValue;
				}
			}

			// Make sure this sub-mf won’t get parsed as a top level mf
			$this->elementPrefixParsed($p, 'p');
		}

		// Handle u-*
		foreach ($this->xpath->query('.//*[contains(concat(" ",  @class)," u-")]', $e) as $u) {
			// element is already parsed
			if ($this->isElementParsed($u, 'u')) {
				continue;
			// backcompat parsing and element was not upgraded; skip it
			} else if ( $is_backcompat && empty($this->upgraded[$u]) ) {
				$this->elementPrefixParsed($u, 'u');
				continue;
			}

			$prefixes[] = 'u-';
			$uValue = $this->parseU($u);

			// Add the value to the array for it’s property types
			foreach (mfNamesFromElement($u, 'u-') as $propName) {
				$return[$propName][] = $uValue;
			}

			// Make sure this sub-mf won’t get parsed as a top level mf
			$this->elementPrefixParsed($u, 'u');
		}

		$temp_dates = array();

		// Handle dt-*
		foreach ($this->xpath->query('.//*[contains(concat(" ", @class), " dt-")]', $e) as $dt) {
			// element is already parsed
			if ($this->isElementParsed($dt, 'dt')) {
				continue;
			// backcompat parsing and element was not upgraded; skip it
			} else if ( $is_backcompat && empty($this->upgraded[$dt]) ) {
				$this->elementPrefixParsed($dt, 'dt');
				continue;
			}

			$prefixes[] = 'dt-';
			$dtValue = $this->parseDT($dt, $dates, $impliedTimezone);

			if ($dtValue) {
				// Add the value to the array for dt- properties
				foreach (mfNamesFromElement($dt, 'dt-') as $propName) {
					$temp_dates[$propName][] = $dtValue;
				}
			}
			// Make sure this sub-mf won’t get parsed as a top level mf
			$this->elementPrefixParsed($dt, 'dt');
		}

		foreach ($temp_dates as $propName => $data) {
			foreach ( $data as $dtValue ) {
				// var_dump(preg_match('/[+-]\d{2}(\d{2})?$/i', $dtValue));
				if ( $impliedTimezone && preg_match('/(Z|[+-]\d{2}:?(\d{2})?)$/i', $dtValue, $matches) == 0 ) {
					$dtValue .= $impliedTimezone;
				}

				$return[$propName][] = $dtValue;
			}
		}

		// Handle e-*
		foreach ($this->xpath->query('.//*[contains(concat(" ", @class)," e-")]', $e) as $em) {
			// element is already parsed
			if ($this->isElementParsed($em, 'e')) {
				continue;
			// backcompat parsing and element was not upgraded; skip it
			} else if ( $is_backcompat && empty($this->upgraded[$em]) ) {
				$this->elementPrefixParsed($em, 'e');
				continue;
			}

			$prefixes[] = 'e-';
			$eValue = $this->parseE($em);

			if ($eValue) {
				// Add the value to the array for e- properties
				foreach (mfNamesFromElement($em, 'e-') as $propName) {
					$return[$propName][] = $eValue;
				}
			}
			// Make sure this sub-mf won’t get parsed as a top level mf
			$this->elementPrefixParsed($em, 'e');
		}

		// Do we need to imply a name property?
		// if no explicit "name" property, and no other p-* or e-* properties, and no nested microformats,
		if (!array_key_exists('name', $return) && !in_array('p-', $prefixes) && !in_array('e-', $prefixes) && !$has_nested_mf && !$is_backcompat) {
			$name = false;
			// img.h-x[alt] or area.h-x[alt]
			if (($e->tagName === 'img' || $e->tagName === 'area') && $e->hasAttribute('alt')) {
				$name = $e->getAttribute('alt');
			// abbr.h-x[title]
			} elseif ($e->tagName === 'abbr' && $e->hasAttribute('title')) {
				$name = $e->getAttribute('title');
			} else {
				$xpaths = array(
					// .h-x>img:only-child[alt]:not([alt=""]):not[.h-*]
					'./img[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and @alt and string-length(@alt) != 0]',
					// .h-x>area:only-child[alt]:not([alt=""]):not[.h-*]
					'./area[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and @alt and string-length(@alt) != 0]',
					// .h-x>abbr:only-child[title]:not([title=""]):not[.h-*]
					'./abbr[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and @title and string-length(@title) != 0]',
					// .h-x>:only-child:not[.h-*]>img:only-child[alt]:not([alt=""]):not[.h-*]
					'./*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(*) = 1]/img[not(contains(concat(" ", @class), " h-")) and @alt and string-length(@alt) != 0]',
					// .h-x>:only-child:not[.h-*]>area:only-child[alt]:not([alt=""]):not[.h-*]
					'./*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(*) = 1]/area[not(contains(concat(" ", @class), " h-")) and @alt and string-length(@alt) != 0]',
					// .h-x>:only-child:not[.h-*]>abbr:only-child[title]:not([title=""]):not[.h-*]
					'./*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(*) = 1]/abbr[not(contains(concat(" ", @class), " h-")) and @title and string-length(@title) != 0]'
				);
				foreach ($xpaths as $xpath) {
					$nameElement = $this->xpath->query($xpath, $e);
					if ($nameElement !== false && $nameElement->length === 1) {
						$nameElement = $nameElement->item(0);
						if ($nameElement->tagName === 'img' || $nameElement->tagName === 'area') {
							$name = $nameElement->getAttribute('alt');
						} else {
							$name = $nameElement->getAttribute('title');
						}
						break;
					}
				}
			}
			if ($name === false) {
				$name = $this->textContent($e, true);
			}
			$return['name'][] = unicodeTrim($name);
		}

		// Check for u-photo
		if (!array_key_exists('photo', $return) && !$is_backcompat) {

			$photo = $this->parseImpliedPhoto($e);

			if ($photo !== false) {
				$return['photo'][] = $photo;
			}

		}

		// Do we need to imply a url property?
		// if no explicit "url" property, and no other explicit u-* properties, and no nested microformats
		if (!array_key_exists('url', $return) && !in_array('u-', $prefixes) && !$has_nested_mf && !$is_backcompat) {
			// a.h-x[href] or area.h-x[href]
			if (($e->tagName === 'a' || $e->tagName === 'area') && $e->hasAttribute('href')) {
				$return['url'][] = $this->resolveUrl($e->getAttribute('href'));
			} else {
				$xpaths = array(
					// .h-x>a[href]:only-of-type:not[.h-*]
					'./a[not(contains(concat(" ", @class), " h-")) and count(../a) = 1 and @href]',
					// .h-x>area[href]:only-of-type:not[.h-*]
					'./area[not(contains(concat(" ", @class), " h-")) and count(../area) = 1 and @href]',
					// .h-x>:only-child:not[.h-*]>a[href]:only-of-type:not[.h-*]
					'./*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(a) = 1]/a[not(contains(concat(" ", @class), " h-")) and @href]',
					// .h-x>:only-child:not[.h-*]>area[href]:only-of-type:not[.h-*]
					'./*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(area) = 1]/area[not(contains(concat(" ", @class), " h-")) and @href]'
				);
				foreach ($xpaths as $xpath) {
					$url = $this->xpath->query($xpath, $e);
					if ($url !== false && $url->length === 1) {
						$return['url'][] = $this->resolveUrl($url->item(0)->getAttribute('href'));
						break;
					}
				}
			}
		}

		// Make sure things are unique and in alphabetical order
		$mfTypes = array_unique($mfTypes);
		sort($mfTypes);

		// Properties should be an object when JSON serialised
		if (empty($return) and $this->jsonMode) {
			$return = new stdClass();
		}

		// Phew. Return the final result.
		$parsed = array(
			'type' => $mfTypes,
			'properties' => $return
		);

		if($this->lang) {
			// Language
			if ( $html_lang = $this->language($e) ) {
				$parsed['lang'] = $html_lang;
			}
		}

		if (!empty($shape)) {
			$parsed['shape'] = $shape;
		}

		if (!empty($coords)) {
			$parsed['coords'] = $coords;
		}

		if (!empty($children)) {
			$parsed['children'] = array_values(array_filter($children));
		}
		return $parsed;
	}

	/**
	 * @see http://microformats.org/wiki/microformats2-parsing#parsing_for_implied_properties
	 */
	public function parseImpliedPhoto(\DOMElement $e) {

		// img.h-x[src]
		if ($e->tagName == 'img') {
			return $this->resolveUrl($e->getAttribute('src'));
		}

		// object.h-x[data]
		if ($e->tagName == 'object' && $e->hasAttribute('data')) {
			return $this->resolveUrl($e->getAttribute('data'));
		}

		$xpaths = array(
			// .h-x>img[src]:only-of-type:not[.h-*]
			'./img[not(contains(concat(" ", @class), " h-")) and count(../img) = 1 and @src]',
			// .h-x>object[data]:only-of-type:not[.h-*]
			'./object[not(contains(concat(" ", @class), " h-")) and count(../object) = 1 and @data]',
			// .h-x>:only-child:not[.h-*]>img[src]:only-of-type:not[.h-*]
			'./*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(img) = 1]/img[not(contains(concat(" ", @class), " h-")) and @src]',
			// .h-x>:only-child:not[.h-*]>object[data]:only-of-type:not[.h-*]
			'./*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(object) = 1]/object[not(contains(concat(" ", @class), " h-")) and @data]',
		);

		foreach ($xpaths as $path) {
			$els = $this->xpath->query($path, $e);

			if ($els !== false && $els->length === 1) {
				$el = $els->item(0);
				if ($el->tagName == 'img') {
					return $this->resolveUrl($el->getAttribute('src'));
				} else if ($el->tagName == 'object') {
					return $this->resolveUrl($el->getAttribute('data'));
				}
			}
		}

		// no implied photo
		return false;
	}

	/**
	 * Parse rels and alternates
	 *
	 * Returns [$rels, $rel_urls, $alternates].
	 * For $rels and $rel_urls, if they are empty and $this->jsonMode = true, they will be returned as stdClass,
	 * optimizing for JSON serialization. Otherwise they will be returned as an empty array.
	 * Note that $alternates is deprecated in the microformats spec in favor of $rel_urls. $alternates only appears
	 * in parsed results if $this->enableAlternates = true.
	 * @return array|stdClass
	 */
	public function parseRelsAndAlternates() {
		$rels = array();
		$rel_urls = array();
		$alternates = array();

		// Iterate through all a, area and link elements with rel attributes
		foreach ($this->xpath->query('//a[@rel and @href] | //link[@rel and @href] | //area[@rel and @href]') as $hyperlink) {
			// Parse the set of rels for the current link
			$linkRels = array_unique(array_filter(preg_split('/[\t\n\f\r ]/', $hyperlink->getAttribute('rel'))));
			if (count($linkRels) === 0) {
				continue;
			}

			// Resolve the href
			$href = $this->resolveUrl($hyperlink->getAttribute('href'));

			$rel_attributes = array();

			if ($hyperlink->hasAttribute('media')) {
				$rel_attributes['media'] = $hyperlink->getAttribute('media');
			}

			if ($hyperlink->hasAttribute('hreflang')) {
				$rel_attributes['hreflang'] = $hyperlink->getAttribute('hreflang');
			}

			if ($hyperlink->hasAttribute('title')) {
				$rel_attributes['title'] = $hyperlink->getAttribute('title');
			}

			if ($hyperlink->hasAttribute('type')) {
				$rel_attributes['type'] = $hyperlink->getAttribute('type');
			}

			if (strlen($hyperlink->textContent) > 0) {
				$rel_attributes['text'] = $hyperlink->textContent;
			}

			if ($this->enableAlternates) {
				// If 'alternate' in rels, create 'alternates' structure, append
				if (in_array('alternate', $linkRels)) {
					$alternates[] = array_merge(
						$rel_attributes,
						array(
							'url' => $href,
							'rel' => implode(' ', array_diff($linkRels, array('alternate')))
						)
					);
				}
			}

			foreach ($linkRels as $rel) {
				if (!array_key_exists($rel, $rels)) {
					$rels[$rel] = array($href);
				} elseif (!in_array($href, $rels[$rel])) {
					$rels[$rel][] = $href;
				}
			}

			if (!array_key_exists($href, $rel_urls)) {
				$rel_urls[$href] = array('rels' => array());
			}

			// Add the attributes collected only if they were not already set
			$rel_urls[$href] = array_merge(
				$rel_attributes,
				$rel_urls[$href]
			);

			// Merge current rels with those already set
			$rel_urls[$href]['rels'] = array_merge(
				$rel_urls[$href]['rels'],
				$linkRels
			);
		}

		// Alphabetically sort the rels arrays after removing duplicates
		foreach ($rel_urls as $href => $object) {
			$rel_urls[$href]['rels'] = array_unique($rel_urls[$href]['rels']);
			sort($rel_urls[$href]['rels']);
		}

		if (empty($rels) and $this->jsonMode) {
			$rels = new stdClass();
		}

		if (empty($rel_urls) and $this->jsonMode) {
			$rel_urls = new stdClass();
		}

		return array($rels, $rel_urls, $alternates);
	}

	/**
	 * Find rel=tag elements that don't have class=category and have an href.
	 * For each element, get the last non-empty URL segment. Append a <data>
	 * element with that value as the category. Uses the mf1 class 'category'
	 * which will then be upgraded to p-category during backcompat.
	 * @param DOMElement $el
	 */
	public function upgradeRelTagToCategory(DOMElement $el) {
		$rel_tag = $this->xpath->query('.//a[contains(concat(" ",normalize-space(@rel)," ")," tag ") and not(contains(concat(" ", normalize-space(@class), " "), " category ")) and @href]', $el);

		if ( $rel_tag->length ) {
			foreach ( $rel_tag as $tempEl ) {
				$path = trim(parse_url($tempEl->getAttribute('href'), PHP_URL_PATH), ' /');
				$segments = explode('/', $path);
				$value = array_pop($segments);

				# build the <data> element
				$dataEl = $tempEl->ownerDocument->createElement('data');
				$dataEl->setAttribute('class', 'category');
				$dataEl->setAttribute('value', $value);

				# append as child of input element. this should ensure added element does get parsed inside e-*
				$el->appendChild($dataEl);
			}
		}
	}

	/**
	 * Kicks off the parsing routine
	 * @param bool $convertClassic whether to do backcompat parsing on microformats1. Defaults to true.
	 * @param DOMElement $context optionally specify an element from which to parse microformats
	 * @return array An array containing all the microformats found in the current document
	 */
	public function parse($convertClassic = true, DOMElement $context = null) {
		$this->convertClassic = $convertClassic;
		$mfs = $this->parse_recursive($context);

		// Parse rels
		list($rels, $rel_urls, $alternates) = $this->parseRelsAndAlternates();

		$top = array(
			'items' => array_values(array_filter($mfs)),
			'rels' => $rels,
			'rel-urls' => $rel_urls,
		);

		if ($this->enableAlternates && count($alternates)) {
			$top['alternates'] = $alternates;
		}

		return $top;
	}

	/**
	 * Parse microformats recursively
	 * Keeps track of whether inside a backcompat root or not
	 * @param DOMElement $context: node to start with
	 * @param int $depth: recursion depth
	 * @return array
	 */
	public function parse_recursive(DOMElement $context = null, $depth = 0) {
		$mfs = array();
		$mfElements = $this->getRootMF($context);

		foreach ($mfElements as $node) {
			$is_backcompat = !$this->hasRootMf2($node);

			if ($this->convertClassic && $is_backcompat) {
				$this->backcompat($node);
			}

			$recurse = $this->parse_recursive($node, $depth + 1);

			// set bool flag for nested mf
			$has_nested_mf = ($recurse);

			// parse for root mf
			$result = $this->parseH($node, $is_backcompat, $has_nested_mf);

			// TODO: Determine if clearing this is required?
			$this->elementPrefixParsed($node, 'h');
			$this->elementPrefixParsed($node, 'p');
			$this->elementPrefixParsed($node, 'u');
			$this->elementPrefixParsed($node, 'dt');
			$this->elementPrefixParsed($node, 'e');

			// parseH returned a parsed result
			if ($result) {

				// merge recursive results into current results
				if ($recurse) {
					$result = array_merge_recursive($result, $recurse);
				}

				// currently a nested mf; check if node is an mf property of parent
				if ($depth > 0) {
					$temp_properties = nestedMfPropertyNamesFromElement($node);

					// properties found; set up parsed result in 'properties'
					if (!empty($temp_properties)) {

						foreach ($temp_properties as $property => $prefixes) {
							// Note: handling microformat nesting under multiple conflicting prefixes is not currently specified by the mf2 parsing spec.
							$prefixSpecificResult = $result;
							if (in_array('p-', $prefixes)) {
								$prefixSpecificResult['value'] = (!is_array($prefixSpecificResult['properties']) || empty($prefixSpecificResult['properties']['name'][0])) ? $this->parseP($node) : $prefixSpecificResult['properties']['name'][0];
							} elseif (in_array('e-', $prefixes)) {
								$eParsedResult = $this->parseE($node);
								$prefixSpecificResult['html'] = $eParsedResult['html'];
								$prefixSpecificResult['value'] = $eParsedResult['value'];
							} elseif (in_array('u-', $prefixes)) {
								$prefixSpecificResult['value'] = (!is_array($result['properties']) || empty($result['properties']['url'])) ? $this->parseU($node) : reset($result['properties']['url']);
							} elseif (in_array('dt-', $prefixes)) {
								$parsed_property = $this->parseDT($node);
								$prefixSpecificResult['value'] = ($parsed_property) ? $parsed_property : '';
							}

							$mfs['properties'][$property][] = $prefixSpecificResult;
						}

					// otherwise, set up in 'children'
					} else {
						$mfs['children'][] = $result;
					}
				// otherwise, top-level mf
				} else {
					$mfs[] = $result;
				}
			}
		}

		return $mfs;
	}

	/**
	 * Parse From ID
	 *
	 * Given an ID, parse all microformats which are children of the element with
	 * that ID.
	 *
	 * Note that rel values are still document-wide.
	 *
	 * If an element with the ID is not found, an empty skeleton mf2 array structure
	 * will be returned.
	 *
	 * @param string $id
	 * @param bool $htmlSafe = false whether or not to HTML-encode angle brackets in non e-* properties
	 * @return array
	 */
	public function parseFromId($id, $convertClassic=true) {
		$matches = $this->xpath->query("//*[@id='{$id}']");

		if (empty($matches))
			return array('items' => array(), 'rels' => array(), 'alternates' => array());

		return $this->parse($convertClassic, $matches->item(0));
	}

	/**
	 * Get the root microformat elements
	 * @param DOMElement $context
	 * @return DOMNodeList
	 */
	public function getRootMF(DOMElement $context = null) {
		// start with mf2 root class name xpath
		$xpaths = array(
			'contains(concat(" ",normalize-space(@class)), " h-")'
		);

		// add mf1 root class names
		foreach ( $this->classicRootMap as $old => $new ) {
			$xpaths[] = '( contains(concat(" ",normalize-space(@class), " "), " ' . $old . ' ") )';
		}

		// final xpath with OR
		$xpath = '//*[' . implode(' or ', $xpaths) . ']';

		$mfElements = (null === $context)
			? $this->xpath->query($xpath)
			: $this->xpath->query('.' . $xpath, $context);

		return $mfElements;
	}

	/**
	 * Apply the backcompat algorithm to upgrade mf1 classes to mf2.
	 * This method is called recursively.
	 * @param DOMElement $el
	 * @param string $context
	 * @param bool $isParentMf2
	 * @see http://microformats.org/wiki/microformats2-parsing#algorithm
	 */
	public function backcompat(DOMElement $el, $context = '', $isParentMf2 = false) {

		if ( $context ) {
			$mf1Classes = array($context);
		} else {
			$class = str_replace(array("\t", "\n"), ' ', $el->getAttribute('class'));
			$classes = array_filter(explode(' ', $class));
			$mf1Classes = array_intersect($classes, array_keys($this->classicRootMap));
		}

		$elHasMf2 = $this->hasRootMf2($el);

		foreach ($mf1Classes as $classname) {
			// special handling for specific properties
			switch ( $classname )
			{
				case 'hentry':
					$this->upgradeRelTagToCategory($el);

					$rel_bookmark = $this->xpath->query('.//a[contains(concat(" ",normalize-space(@rel)," ")," bookmark ") and @href]', $el);

					if ( $rel_bookmark->length ) {
						foreach ( $rel_bookmark as $tempEl ) {
							$this->addMfClasses($tempEl, 'u-url');
							$this->addUpgraded($tempEl, array('bookmark'));
						}
					}
				break;

				case 'hreview':
					$item_and_vcard = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " item ") and contains(concat(" ", normalize-space(@class), " "), " vcard ")]', $el);

					if ( $item_and_vcard->length ) {
						foreach ( $item_and_vcard as $tempEl ) {
							if ( !$this->hasRootMf2($tempEl) ) {
								$this->backcompat($tempEl, 'vcard');
								$this->addMfClasses($tempEl, 'p-item h-card');
								$this->addUpgraded($tempEl, array('item', 'vcard'));
							}
						}
					}

					$item_and_vevent = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " item ") and contains(concat(" ", normalize-space(@class), " "), " vevent ")]', $el);

					if ( $item_and_vevent->length ) {
						foreach ( $item_and_vevent as $tempEl ) {
							if ( !$this->hasRootMf2($tempEl) ) {
								$this->addMfClasses($tempEl, 'p-item h-event');
								$this->backcompat($tempEl, 'vevent');
								$this->addUpgraded($tempEl, array('item', 'vevent'));
							}
						}
					}

					$item_and_hproduct = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " item ") and contains(concat(" ", normalize-space(@class), " "), " hproduct ")]', $el);

					if ( $item_and_hproduct->length ) {
						foreach ( $item_and_hproduct as $tempEl ) {
							if ( !$this->hasRootMf2($tempEl) ) {
								$this->addMfClasses($tempEl, 'p-item h-product');
								$this->backcompat($tempEl, 'vevent');
								$this->addUpgraded($tempEl, array('item', 'hproduct'));
							}
						}
					}

					$this->upgradeRelTagToCategory($el);
				break;

				case 'vevent':
					$location = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " location ")]', $el);

					if ( $location->length ) {
						foreach ( $location as $tempEl ) {
							if ( !$this->hasRootMf2($tempEl) ) {
								$this->addMfClasses($tempEl, 'h-card');
								$this->backcompat($tempEl, 'vcard');
							}
						}
					}
				break;
			}

			// root class has mf1 properties to be upgraded
			if ( isset($this->classicPropertyMap[$classname]) ) {
				// loop through each property of the mf1 root
				foreach ( $this->classicPropertyMap[$classname] as $property => $data ) {
					$propertyElements = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " ' . $property . ' ")]', $el);

					// loop through each element with the property
					foreach ( $propertyElements as $propertyEl ) {
						$hasRootMf2 = $this->hasRootMf2($propertyEl);

						// if the element has not been upgraded and we're not inside an mf2 root, recurse
						if ( !$this->isElementUpgraded($propertyEl, $property) && !$isParentMf2 )
						{
							$temp_context = ( isset($data['context']) ) ? $data['context'] : null;
							$this->backcompat($propertyEl, $temp_context, $hasRootMf2);
							$this->addMfClasses($propertyEl, $data['replace']);
						}

						$this->addUpgraded($propertyEl, $property);
					}
				}
			}

			if ( empty($context) && isset($this->classicRootMap[$classname]) && !$elHasMf2 ) {
				$this->addMfClasses($el, $this->classicRootMap[$classname]);
			}
		}

		return;
	}

	/**
	 * Add element + property as upgraded during backcompat
	 * @param DOMElement $el
	 * @param string|array $property
	 */
	public function addUpgraded(DOMElement $el, $property) {
		if ( !is_array($property) ) {
			$property = array($property);
		}

		// add element to list of upgraded elements
		if ( !$this->upgraded->contains($el) ) {
			$this->upgraded->attach($el, $property);
		} else {
			$this->upgraded[$el] = array_merge($this->upgraded[$el], $property);
		}
	}

	/**
	 * Add the provided classes to an element.
	 * Does not add duplicate if class name already exists.
	 * @param DOMElement $el
	 * @param string $classes
	 */
	public function addMfClasses(DOMElement $el, $classes) {
		$existingClasses = str_replace(array("\t", "\n"), ' ', $el->getAttribute('class'));
		$existingClasses = array_filter(explode(' ', $existingClasses));

		$addClasses = array_diff(explode(' ', $classes), $existingClasses);

		if ( $addClasses ) {
			$el->setAttribute('class', $el->getAttribute('class') . ' ' . implode(' ', $addClasses));
		}
	}

	/**
	 * Check an element for mf2 h-* class, typically to determine if backcompat should be used
	 * @param DOMElement $el
	 */
	public function hasRootMf2(\DOMElement $el) {
		$class = str_replace(array("\t", "\n"), ' ', $el->getAttribute('class'));
		$classes = array_filter(explode(' ', $class));

		foreach ( $classes as $classname ) {
			if ( strpos($classname, 'h-') === 0 ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Convert Legacy Classnames
	 *
	 * Adds microformats2 classnames into a document containing only legacy
	 * semantic classnames.
	 *
	 * @return Parser $this
	 */
	public function convertLegacy() {
		$doc = $this->doc;
		$xp = new DOMXPath($doc);

		// replace all roots
		foreach ($this->classicRootMap as $old => $new) {
			foreach ($xp->query('//*[contains(concat(" ", @class, " "), " ' . $old . ' ") and not(contains(concat(" ", @class, " "), " ' . $new . ' "))]') as $el) {
				$el->setAttribute('class', $el->getAttribute('class') . ' ' . $new);
			}
		}

		foreach ($this->classicPropertyMap as $oldRoot => $properties) {
			$newRoot = $this->classicRootMap[$oldRoot];
			foreach ($properties as $old => $data) {
				foreach ($xp->query('//*[contains(concat(" ", @class, " "), " ' . $oldRoot . ' ")]//*[contains(concat(" ", @class, " "), " ' . $old . ' ") and not(contains(concat(" ", @class, " "), " ' . $data['replace'] . ' "))]') as $el) {
					$el->setAttribute('class', $el->getAttribute('class') . ' ' . $data['replace']);
				}
			}
		}

		return $this;
	}

	/**
	 * XPath Query
	 *
	 * Runs an XPath query over the current document. Works in exactly the same
	 * way as DOMXPath::query.
	 *
	 * @param string $expression
	 * @param DOMNode $context
	 * @return DOMNodeList
	 */
	public function query($expression, $context = null) {
		return $this->xpath->query($expression, $context);
	}

	/**
	 * Classic Root Classname map
	 * @var array
	 */
	public $classicRootMap = array(
		'vcard' => 'h-card',
		'hfeed' => 'h-feed',
		'hentry' => 'h-entry',
		'hrecipe' => 'h-recipe',
		'hresume' => 'h-resume',
		'vevent' => 'h-event',
		'hreview' => 'h-review',
		'hproduct' => 'h-product',
		'adr' => 'h-adr',
	);

	/**
	 * Mapping of mf1 properties to mf2 and the context they're parsed with
	 * @var array
	 */
	public $classicPropertyMap = array(
		'vcard' => array(
			'fn' => array(
				'replace' => 'p-name'
			),
			'honorific-prefix' => array(
				'replace' => 'p-honorific-prefix'
			),
			'given-name' => array(
				'replace' => 'p-given-name'
			),
			'additional-name' => array(
				'replace' => 'p-additional-name'
			),
			'family-name' => array(
				'replace' => 'p-family-name'
			),
			'honorific-suffix' => array(
				'replace' => 'p-honorific-suffix'
			),
			'nickname' => array(
				'replace' => 'p-nickname'
			),
			'email' => array(
				'replace' => 'u-email'
			),
			'logo' => array(
				'replace' => 'u-logo'
			),
			'photo' => array(
				'replace' => 'u-photo'
			),
			'url' => array(
				'replace' => 'u-url'
			),
			'uid' => array(
				'replace' => 'u-uid'
			),
			'category' => array(
				'replace' => 'p-category'
			),
			'adr' => array(
				'replace' => 'p-adr',
			),
			'extended-address' => array(
				'replace' => 'p-extended-address'
			),
			'street-address' => array(
				'replace' => 'p-street-address'
			),
			'locality' => array(
				'replace' => 'p-locality'
			),
			'region' => array(
				'replace' => 'p-region'
			),
			'postal-code' => array(
				'replace' => 'p-postal-code'
			),
			'country-name' => array(
				'replace' => 'p-country-name'
			),
			'label' => array(
				'replace' => 'p-label'
			),
			'geo' => array(
				'replace' => 'p-geo h-geo'
			),
			'latitude' => array(
				'replace' => 'p-latitude'
			),
			'longitude' => array(
				'replace' => 'p-longitude'
			),
			'tel' => array(
				'replace' => 'p-tel'
			),
			'note' => array(
				'replace' => 'p-note'
			),
			'bday' => array(
				'replace' => 'dt-bday'
			),
			'key' => array(
				'replace' => 'u-key'
			),
			'org' => array(
				'replace' => 'p-org'
			),
			'organization-name' => array(
				'replace' => 'p-organization-name'
			),
			'organization-unit' => array(
				'replace' => 'p-organization-unit'
			),
			'title' => array(
				'replace' => 'p-job-title'
			),
			'role' => array(
				'replace' => 'p-role'
			),
			'tz' => array(
				'replace' => 'p-tz'
			),
			'rev' => array(
				'replace' => 'dt-rev'
			),
		),
		'hfeed' => array(
			# nothing currently
		),
		'hentry' => array(
			'entry-title' => array(
				'replace' => 'p-name'
			),
			'entry-summary' => array(
				'replace' => 'p-summary'
			),
			'entry-content' => array(
				'replace' => 'e-content'
			),
			'published' => array(
				'replace' => 'dt-published'
			),
			'updated' => array(
				'replace' => 'dt-updated'
			),
			'author' => array(
				'replace' => 'p-author h-card',
				'context' => 'vcard',
			),
			'category' => array(
				'replace' => 'p-category'
			),
		),
		'hrecipe' => array(
			'fn' => array(
				'replace' => 'p-name'
			),
			'ingredient' =>  array(
				'replace' => 'p-ingredient'
				/**
				 * TODO: hRecipe 'value' and 'type' child mf not parsing correctly currently.
				 * Per http://microformats.org/wiki/hRecipe#Property_details, they're experimental.
				 */
			),
			'yield' =>  array(
				'replace' => 'p-yield'
			),
			'instructions' =>  array(
				'replace' => 'e-instructions'
			),
			'duration' =>  array(
				'replace' => 'dt-duration'
			),
			'photo' =>  array(
				'replace' => 'u-photo'
			),
			'summary' =>  array(
				'replace' => 'p-summary'
			),
			'author' =>  array(
				'replace' => 'p-author h-card',
				'context' => 'vcard',
			),
			'nutrition' =>  array(
				'replace' => 'p-nutrition'
			),
			'category' =>  array(
				'replace' => 'p-category'
			),
		),
		'hresume' => array(
			'summary' => array(
				'replace' => 'p-summary'
			),
			'contact' => array(
				'replace' => 'p-contact h-card',
				'context' => 'vcard',
			),
			'education' => array(
				'replace' => 'p-education h-event',
				'context' => 'vevent',
			),
			'experience' => array(
				'replace' => 'p-experience h-event',
				'context' => 'vevent',
			),
			'skill' => array(
				'replace' => 'p-skill'
			),
			'affiliation' => array(
				'replace' => 'p-affiliation h-card',
				'context' => 'vcard',
			),
		),
		'vevent' => array(
			'summary' => array(
				'replace' => 'p-name'
			),
			'dtstart' => array(
				'replace' => 'dt-start'
			),
			'dtend' => array(
				'replace' => 'dt-end'
			),
			'duration' => array(
				'replace' => 'dt-duration'
			),
			'description' => array(
				'replace' => 'p-description'
			),
			'url' => array(
				'replace' => 'u-url'
			),
			'category' => array(
				'replace' => 'p-category'
			),
			'location' => array(
				'replace' => 'h-card',
				'context' => 'vcard'
			),
			'geo' => array(
				'replace' => 'p-location h-geo'
			),
		),
		'hreview' => array(
			'summary' => array(
				'replace' => 'p-name'
			),
			# fn: see item.fn below
			# photo: see item.photo below
			# url: see item.url below
			'item' => array(
				'replace' => 'p-item h-item',
				'context' => 'item'
			),
			'reviewer' => array(
				'replace' => 'p-author h-card',
				'context' => 'vcard',
			),
			'dtreviewed' => array(
				'replace' => 'dt-published'
			),
			'rating' => array(
				'replace' => 'p-rating'
			),
			'best' => array(
				'replace' => 'p-best'
			),
			'worst' => array(
				'replace' => 'p-worst'
			),
			'description' => array(
				'replace' => 'e-content'
			),
			'category' => array(
				'replace' => 'p-category'
			),
		),
		'hproduct' => array(
			'fn' => array(
				'replace' => 'p-name',
			),
			'photo' => array(
				'replace' => 'u-photo',
			),
			'brand' => array(
				'replace' => 'p-brand',
			),
			'category' => array(
				'replace' => 'p-category',
			),
			'description' => array(
				'replace' => 'p-description',
			),
			'identifier' => array(
				'replace' => 'u-identifier',
			),
			'url' => array(
				'replace' => 'u-url',
			),
			'review' => array(
				'replace' => 'p-review h-review',
			),
			'price' => array(
				'replace' => 'p-price'
			),
		),
		'item' => array(
			'fn' => array(
				'replace' => 'p-name'
			),
			'url' => array(
				'replace' => 'u-url'
			),
			'photo' => array(
				'replace' => 'u-photo'
			),
		),
		'adr' => array(
			'post-office-box' => array(
				'replace' => 'p-post-office-box'
			),
			'extended-address' => array(
				'replace' => 'p-extended-address'
			),
			'street-address' => array(
				'replace' => 'p-street-address'
			),
			'locality' => array(
				'replace' => 'p-locality'
			),
			'region' => array(
				'replace' => 'p-region'
			),
			'postal-code' => array(
				'replace' => 'p-postal-code'
			),
			'country-name' => array(
				'replace' => 'p-country-name'
			),
		),
		'geo' => array(
			'latitude' => array(
				'replace' => 'p-latitude'
			),
			'longitude' => array(
				'replace' => 'p-longitude'
			),
		),
	);
}

function parseUriToComponents($uri) {
	$result = array(
		'scheme' => null,
		'authority' => null,
		'path' => null,
		'query' => null,
		'fragment' => null
	);

	$u = @parse_url($uri);

	if(array_key_exists('scheme', $u))
		$result['scheme'] = $u['scheme'];

	if(array_key_exists('host', $u)) {
		if(array_key_exists('user', $u))
			$result['authority'] = $u['user'];
		if(array_key_exists('pass', $u))
			$result['authority'] .= ':' . $u['pass'];
		if(array_key_exists('user', $u) || array_key_exists('pass', $u))
			$result['authority'] .= '@';
		$result['authority'] .= $u['host'];
		if(array_key_exists('port', $u))
			$result['authority'] .= ':' . $u['port'];
	}

	if(array_key_exists('path', $u))
		$result['path'] = $u['path'];

	if(array_key_exists('query', $u))
		$result['query'] = $u['query'];

	if(array_key_exists('fragment', $u))
		$result['fragment'] = $u['fragment'];

	return $result;
}

function resolveUrl($baseURI, $referenceURI) {
	$target = array(
		'scheme' => null,
		'authority' => null,
		'path' => null,
		'query' => null,
		'fragment' => null
	);

	# 5.2.1 Pre-parse the Base URI
	# The base URI (Base) is established according to the procedure of
	# Section 5.1 and parsed into the five main components described in
	# Section 3
	$base = parseUriToComponents($baseURI);

	# If base path is blank (http://example.com) then set it to /
	# (I can't tell if this is actually in the RFC or not, but seems like it makes sense)
	if($base['path'] == null)
		$base['path'] = '/';

	# 5.2.2. Transform References

	# The URI reference is parsed into the five URI components
	# (R.scheme, R.authority, R.path, R.query, R.fragment) = parse(R);
	$reference = parseUriToComponents($referenceURI);

	# A non-strict parser may ignore a scheme in the reference
	# if it is identical to the base URI's scheme.
	# TODO

	if($reference['scheme']) {
		$target['scheme'] = $reference['scheme'];
		$target['authority'] = $reference['authority'];
		$target['path'] = removeDotSegments($reference['path']);
		$target['query'] = $reference['query'];
	} else {
		if($reference['authority']) {
			$target['authority'] = $reference['authority'];
			$target['path'] = removeDotSegments($reference['path']);
			$target['query'] = $reference['query'];
		} else {
			if($reference['path'] == '') {
				$target['path'] = $base['path'];
				if($reference['query']) {
					$target['query'] = $reference['query'];
				} else {
					$target['query'] = $base['query'];
				}
			} else {
				if(substr($reference['path'], 0, 1) == '/') {
					$target['path'] = removeDotSegments($reference['path']);
				} else {
					$target['path'] = mergePaths($base, $reference);
					$target['path'] = removeDotSegments($target['path']);
				}
				$target['query'] = $reference['query'];
			}
			$target['authority'] = $base['authority'];
		}
		$target['scheme'] = $base['scheme'];
	}
	$target['fragment'] = $reference['fragment'];

	# 5.3 Component Recomposition
	$result = '';
	if($target['scheme']) {
		$result .= $target['scheme'] . ':';
	}
	if($target['authority']) {
		$result .= '//' . $target['authority'];
	}
	$result .= $target['path'];
	if($target['query']) {
		$result .= '?' . $target['query'];
	}
	if($target['fragment']) {
		$result .= '#' . $target['fragment'];
	} elseif($referenceURI == '#') {
		$result .= '#';
	}
	return $result;
}

# 5.2.3 Merge Paths
function mergePaths($base, $reference) {
	# If the base URI has a defined authority component and an empty
	# path,
	if($base['authority'] && $base['path'] == null) {
		# then return a string consisting of "/" concatenated with the
		# reference's path; otherwise,
		$merged = '/' . $reference['path'];
	} else {
		if(($pos=strrpos($base['path'], '/')) !== false) {
			# return a string consisting of the reference's path component
			# appended to all but the last segment of the base URI's path (i.e.,
			# excluding any characters after the right-most "/" in the base URI
			# path,
			$merged = substr($base['path'], 0, $pos + 1) . $reference['path'];
		} else {
			# or excluding the entire base URI path if it does not contain
			# any "/" characters).
			$merged = $base['path'];
		}
	}
	return $merged;
}

# 5.2.4.A Remove leading ../ or ./
function removeLeadingDotSlash(&$input) {
	if(substr($input, 0, 3) == '../') {
		$input = substr($input, 3);
	} elseif(substr($input, 0, 2) == './') {
		$input = substr($input, 2);
	}
}

# 5.2.4.B Replace leading /. with /
function removeLeadingSlashDot(&$input) {
	if(substr($input, 0, 3) == '/./') {
		$input = '/' . substr($input, 3);
	} else {
		$input = '/' . substr($input, 2);
	}
}

# 5.2.4.C Given leading /../ remove component from output buffer
function removeOneDirLevel(&$input, &$output) {
	if(substr($input, 0, 4) == '/../') {
		$input = '/' . substr($input, 4);
	} else {
		$input = '/' . substr($input, 3);
	}
	$output = substr($output, 0, strrpos($output, '/'));
}

# 5.2.4.D Remove . and .. if it's the only thing in the input
function removeLoneDotDot(&$input) {
	if($input == '.') {
		$input = substr($input, 1);
	} else {
		$input = substr($input, 2);
	}
}

# 5.2.4.E Move one segment from input to output
function moveOneSegmentFromInput(&$input, &$output) {
	if(substr($input, 0, 1) != '/') {
		$pos = strpos($input, '/');
	} else {
		$pos = strpos($input, '/', 1);
	}

	if($pos === false) {
		$output .= $input;
		$input = '';
	} else {
		$output .= substr($input, 0, $pos);
		$input = substr($input, $pos);
	}
}

# 5.2.4 Remove Dot Segments
function removeDotSegments($path) {
	# 1.  The input buffer is initialized with the now-appended path
	#     components and the output buffer is initialized to the empty
	#     string.
	$input = $path;
	$output = '';

	$step = 0;

	# 2.  While the input buffer is not empty, loop as follows:
	while($input) {
		$step++;

		if(substr($input, 0, 3) == '../' || substr($input, 0, 2) == './') {
			#     A.  If the input buffer begins with a prefix of "../" or "./",
			#         then remove that prefix from the input buffer; otherwise,
			removeLeadingDotSlash($input);
		} elseif(substr($input, 0, 3) == '/./' || $input == '/.') {
			#     B.  if the input buffer begins with a prefix of "/./" or "/.",
			#         where "." is a complete path segment, then replace that
			#         prefix with "/" in the input buffer; otherwise,
			removeLeadingSlashDot($input);
		} elseif(substr($input, 0, 4) == '/../' || $input == '/..') {
			#     C.  if the input buffer begins with a prefix of "/../" or "/..",
			#          where ".." is a complete path segment, then replace that
			#          prefix with "/" in the input buffer and remove the last
			#          segment and its preceding "/" (if any) from the output
			#          buffer; otherwise,
			removeOneDirLevel($input, $output);
		} elseif($input == '.' || $input == '..') {
			#     D.  if the input buffer consists only of "." or "..", then remove
			#         that from the input buffer; otherwise,
			removeLoneDotDot($input);
		} else {
			#     E.  move the first path segment in the input buffer to the end of
			#         the output buffer and any subsequent characters up to, but not including,
			#         the next "/" character or the end of the input buffer
			moveOneSegmentFromInput($input, $output);
		}
	}

	return $output;
}


code:
# php-mf2

[![Build Status](https://travis-ci.org/microformats/php-mf2.png?branch=master)](http://travis-ci.org/microformats/php-mf2)

php-mf2 is a pure, generic [microformats-2](http://microformats.org/wiki/microformats-2) parser. It makes HTML as easy to consume as JSON.

Instead of having a hard-coded list of all the different microformats, it follows a set of procedures to handle different property types (e.g. `p-` for plaintext, `u-` for URL, etc). This allows for a very small and maintainable parser.

## Installation

There are two ways of installing php-mf2. I **highly recommend** installing php-mf2 using [Composer](http://getcomposer.org). The rest of the documentation assumes that you have done so.

To install using Composer, run

```
composer require mf2/mf2
```

If you can’t or don’t want to use Composer, then php-mf2 can be installed the old way by downloading [`/Mf2/Parser.php`](https://raw.githubusercontent.com/microformats/php-mf2/master/Mf2/Parser.php), adding it to your project and requiring it from files you want to call its functions from, like this:

```php
<?php

require_once 'Mf2/Parser.php';

// Now all the functions documented below are available, for example:
$mf = Mf2\fetch('https://waterpigs.co.uk');
```

It is recommended to install the HTML5 parser for proper handling of HTML5 elements. Using composer, run

```
composer require masterminds/html5
```

If this library is added to your project, the php-mf2 parser will use it automatically instead of the built-in HTML parser.

### Signed Code Verification

From v0.2.9, php-mf2’s version tags are signed using GPG, allowing you to cryptographically verify that you’re using code which hasn’t been tampered with. To verify the code you will need the GPG keys for one of the people in the list of code signers:

* Barnaby Walters barnaby@waterpigs.co.uk 1C00 430B 19C6 B426 922F E534 BEF8 CE58 118A D524
* Aaron Parecki aaron@parecki.com F384 12A1 55FB 8B15 B7DD 8E07 4225 2B5E 65CE 0ADD
* Bear bear@bear.im 0A93 9BA7 8203 FCBC 58A9 E8B5 9D1E 0661 8EE5 B4D8

To import the relevant keys into your GPG keychain, execute the following command:

```bash
gpg --recv-keys 1C00430B19C6B426922FE534BEF8CE58118AD524 F38412A155FB8B15B7DD8E0742252B5E65CE0ADD 0A939BA78203FCBC58A9E8B59D1E06618EE5B4D8
```

Then verify the installed files like this:

```bash
# in your project root
cd vendor/mf2/mf2
git tag -v v0.3.0
```

If nothing went wrong, you should see the tag commit message, ending something like this:

```
gpg: Signature made Wed  6 Aug 10:04:20 2014 GMT using RSA key ID 2B2BBB65
gpg: Good signature from "Barnaby Walters <barnaby@waterpigs.co.uk>"
gpg:                 aka "[jpeg image of size 12805]"
```

Possible issues:

* **Git complains that there’s no such tag**: check for a .git file in the source folder; odds are you have the prefer-dist setting enabled and composer is just extracting a zip rather than checking out from git.
* **Git complains the gpg command doesn’t exist**: If you successfully imported my key then you obviously do have gpg installed, but you might have gpg2, whereas git looks for gpg. Solution: tell git which binary to use: `git config --global gpg.program 'gpg2'`

## Usage

php-mf2 is PSR-0 autoloadable, so simply include Composer’s auto-generated autoload file (`/vendor/autoload.php`) and you can start using it. These two functions cover most situations:

* To fetch microformats from a URL, call `Mf2\fetch($url)`
* To parse microformats from HTML, call `Mf2\parse($html, $url)`, where `$url` is the URL from which `$html` was loaded, if any. This parameter is required for correct relative URL parsing and must not be left out unless parsing HTML which is not loaded from the web.

## Examples

### Fetching microformats from a URL

```php
<?php

namespace YourApp;

require '/vendor/autoload.php';

use Mf2;

// (Above code (or equivalent) assumed in future examples)

$mf = Mf2\fetch('http://microformats.org');

foreach ($mf['items'] as $microformat) {
	echo "A {$microformat['type'][0]} called {$microformat['properties']['name'][0]}\n";
}

```

### Parsing microformats from a HTML string

Here we demonstrate parsing of microformats2 implied property parsing, where an entire h-card with name and URL properties is created using a single `h-card` class.

```php
<?php

$html = '<a class="h-card" href="https://waterpigs.co.uk/">Barnaby Walters</a>';
$output = Mf2\parse($html, 'https://waterpigs.co.uk/');
```

`$output` is a canonical microformats2 array structure like:

```json
{
	"items": [
		{
			"type": ["h-card"],
			"properties": {
				"name": ["Barnaby Walters"],
				"url": ["https://waterpigs.co.uk/"]
			}
		}
	],
	"rels": {}
}
```

If no microformats are found, `items` will be an empty array.

Note that, whilst the property prefixes are stripped, the prefix of the `h-*` classname(s) in the "type" array are retained.

### Parsing a document with relative URLs

Most of the time you’ll be getting your input HTML from a URL. You should pass that URL as the second parameter to `Mf2\parse()` so that any relative URLs in the document can be resolved. For example, say you got the following HTML from `http://example.org/post/1`:

```html
<div class="h-card">
	<h1 class="p-name">Mr. Example</h1>
	<img class="u-photo" alt="" src="/photo.png" />
</div>
```

Parsing like this:

```php
$output = Mf2\parse($html, 'http://example.org/post/1');
```

will result in the following output, with relative URLs made absolute:

```json
{
	"items": [{
		"type": ["h-card"],
		"properties": {
			"name": ["Mr. Example"],
			"photo": ["http://example.org/photo.png"]
		}
	}],
	"rels": {},
	"rel-urls": {}
}
```

php-mf2 correctly handles relative URL resolution according to the URI and HTML specs, including correct use of the `<base>` element.

### Parsing `rel` and `rel=alternate` values

php-mf2 also parses any link relations in the document, placing them into two top-level arrays — one for `rel=alternate` and another for all other rel values, e.g. when parsing:

```html
<a rel="me" href="https://twitter.com/barnabywalters">Me on twitter</a>
<link rel="alternate etc" href="http://example.com/notes.atom" />
```

parsing will result in the following keys:

```json
{
	"items": [],
	"rels": {
		"me": ["https://twitter.com/barnabywalters"]
	},
	"rel-urls": {
		"https://twitter.com/barnabywalters": {
			"text": "Me on twitter",
			"rels": ["me"]
		},
		"http://example.com/notes.atom": {
			"rels": ["alternate","etc"]
		}
	}
}
```

Protip: if you’re not bothered about the microformats2 data and just want rels and alternates, you can improve performance by creating a `Mf2\Parser` object (see below) and calling `->parseRelsAndAlternates()` instead of `->parse()`, e.g.

```php
<?php

$parser = new Mf2\Parser('<link rel="…');
$relsAndAlternates = $parser->parseRelsAndAlternates();
```

### Debugging Mf2\fetch

`Mf2\fetch()` will attempt to parse any response served with “HTML” in the content-type, regardless of what the status code is. If it receives a non-HTML response it will return null.

To learn what the HTTP status code for any request was, or learn more about the request, pass a variable name as the third parameter to `Mf2\fetch()` — this will be filled with the contents of `curl_getinfo()`, e.g:

```php

<?php

$mf = Mf2\fetch('http://waterpigs.co.uk/this-page-doesnt-exist', true, $curlInfo);
if ($curlInfo['http_code'] == '404') {
	// This page doesn’t exist.
}

```

If it was HTML then it is still parsed, as there are cases where error pages contain microformats — for example a deleted h-entry resulting in a 410 Gone response containing a stub h-entry with amn explanation for the deletion.

### Getting more control by creating a Parser object

The `Mf2\parse()` function covers the most common usage patterns by internally creating an instance of `Mf2\Parser` and returning the output all in one step. For some advanced usage you can also create an instance of `Mf2\Parser` yourself.

The constructor takes two arguments, the input HTML (or a DOMDocument) and the URL to use as a base URL. Once you have a parser, there are a few other things you can do:

### Selectively parsing a document

There are several ways to selectively parse microformats from a document. If you wish to only parse microformats from an element with a particular ID, `Parser::parseFromId($id) ` is the easiest way.

If your needs are more complex, `Parser::parse` accepts an optional context DOMNode as its second parameter. Typically you’d use `Parser::query` to run XPath queries on the document to get the element you want to parse from under, then pass it to `Parser::parse`. Example usage:

```php
$doc = 'More microformats, more microformats <div id="parse-from-here"><span class="h-card">This shows up</span></div> yet more ignored content';
$parser = new Mf2\Parser($doc);

$parser->parseFromId('parse-from-here'); // returns a document with only the h-card descended from div#parse-from-here

$elementIWant = $parser->query('an xpath query')[0];

$parser->parse(true, $elementIWant); // returns a document with only the Microformats under the selected element

```

### Experimental Language Parsing

There is still [ongoing brainstorming](http://microformats.org/wiki/microformats2-parsing-brainstorming#Parse_language_information) around how HTML language attributes should be added to the parsed result. In order to use this feature, you will need to set a flag to opt in.

```php
$doc = '<div class="h-entry" lang="sv" id="postfrag123">
	<h1 class="p-name">En svensk titel</h1>
	<div class="e-content" lang="en">With an <em>english</em> summary</div>
	<div class="e-content">Och <em>svensk</em> huvudtext</div>
</div>';
$parser = new Mf2\Parser($doc);
$parser->lang = true;
$result = $parser->parse();
```

```json
{
	"items": [
		{
			"type": ["h-entry"],
			"properties": {
				"name": ["En svensk titel"],
				"content": [
					{
						"html": "With an <em>english</em> summary",
						"value": "With an english summary",
						"lang": "en"
					},
					{
						"html": "Och <em>svensk</em> huvudtext",
						"value": "Och svensk huvudtext",
						"lang": "sv"
					}
				]
			},
			"lang": "sv"
		}
	],
	"rels": {},
	"rel-urls": {}
}
```

Note that this option is still considered experimental and in development, and the parsed output may change between minor releases.

### Generating output for JSON serialization with JSON-mode

Due to a quirk with the way PHP arrays work, there is an edge case ([reported](https://github.com/microformats/php-mf2/issues/29) by Tom Morris) in which a document with no rel values, when serialised as JSON, results in an empty object as the rels value rather than an empty array. Replacing this in code with a stdClass breaks PHP iteration over the values.

As of version 0.2.6, the default behaviour is back to being PHP-friendly, so if you want to produce results specifically for serialisation as JSON (for example if you run a HTML -> JSON service, or want to run tests against JSON fixtures), enable JSON mode:

```php
// …by passing true as the third constructor:
$jsonParser = new Mf2\Parser($html, $url, true);
```

### Classic Microformats Markup

php-mf2 has some support for parsing classic microformats markup. It’s enabled by default, but can be turned off by calling `Mf2\parse($html, $url, false);` or `$parser->parse(false);` if you’re instanciating a parser yourself.

In previous versions of php-mf2 you could also add your own class mappings — officially this is no longer supported.

* If the built in mappings don’t successfully parse some classic microformats markup then raise an issue and we’ll fix it.
* If you want to screen-scrape websites which don’t use mf2 into mf2 data structures, consider contributing to [php-mf2-shim](https://github.com/microformats/php-mf2-shim)
* If you *really* need to make one-off changes to the default mappings… It is possible. But you have to figure it out for yourself ;)

## Security

**No filtering of content takes place in mf2\Parser, so treat its output as you would any untrusted data from the source of the parsed document.**

Some tips:

* All content apart from the 'html' key in dictionaries produced by parsing an `e-*` property is not HTML-escaped. For example, `<span class="p-name"><code></span>` will result in `"name": ["<code>"]`. At the very least, HTML-escape all properties before echoing them out in HTML
* If you’re using the raw HTML content under the 'html' key of dictionaries produced by parsing `e-*` properties, you SHOULD purify the HTML before displaying it to prevent injection of arbitrary code. For PHP I recommend using [HTML Purifier](http://htmlpurifier.org)

TODO: move this section to a security/consumption best practises page on the wiki

## Contributing

Issues and bug reports are very welcome. If you know how to write tests then please do so as code always expresses problems and intent much better than English, and gives me a way of measuring whether or not fixes have actually solved your problem. If you don’t know how to write tests, don’t worry :) Just include as much useful information in the issue as you can.

Pull requests very welcome, please try to maintain stylistic, structural and naming consistency with the existing codebase, and don’t be too upset if I make naming changes :)

### How to make a Pull Request

1. Fork the repo to your github account
2. Clone a copy to your computer (simply installing php-mf2 using composer only works for using it, not developing it)
3. Install the dev dependencies with `./composer.phar install`
4. Run PHPUnit with `./vendor/bin/phpunit`
5. Make your changes
6. Add PHPUnit tests for your changes, either in an existing test file if suitable, or a new one
7. Make sure your tests pass (`./vendor/bin/phpunit`), using 5.4+
8. Go to your fork of the repo on github.com and make a pull request, preferably with a short summary, detailed description and references to issues/parsing specs as appropriate
9. Bask in the warm feeling of having contributed to a piece of free software

### Testing

There are currently two separate test suites: one, in `tests/Mf2`, is written in phpunit, containing many microformats parsing examples as well as internal parser tests and regression tests for specific issues over php-mf2’s history. Run it with `./vendor/bin/phpunit`. If you do not have a live internet connection, you can exclude tests that depend on it: `./vendor/bin/phpunit --exclude-group internet`.

The other, in `tests/test-suite`, is a custom test harness which hooks up php-mf2 to the cross-platform [microformats test suite](https://github.com/microformats/tests). To run these tests you must first install the tests with `./composer.phar install`. Each test consists of a HTML file and a corresponding JSON file, and the suite can be run with `php ./tests/test-suite/test-suite.php`.

Currently php-mf2 passes the majority of it’s own test case, and a good percentage of the cross-platform tests. Contributors should ALWAYS test against the PHPUnit suite to ensure any changes don’t negatively impact php-mf2, and SHOULD run the cross-platform suite, especially if you’re changing parsing behaviour.

### Changelog

#### v0.4.6

Bugfixes:

* Don't include img src attribute in implied p-name ([#180](https://github.com/microformats/php-mf2/issues/180))
* Normalize ordinal dates in VCP values ([#167](https://github.com/microformats/php-mf2/issues/167))
* Fix for accidental array access of stdClass in deeply nested structures ([#196](https://github.com/microformats/php-mf2/issues/196))
* Reduce instances where u-url is implied according to a [spec update](http://microformats.org/wiki/index.php?title=microformats2-parsing&diff=66887&oldid=66871) ([#183](https://github.com/microformats/php-mf2/issues/183) and [parsing issue #36](https://github.com/microformats/microformats2-parsing/issues/36))
* Fix for wrongly implied photo property ([#190](https://github.com/microformats/php-mf2/issues/190))

Other Updates:

* Adds a filter to avoid running tests that require a live internet connection ([#194](https://github.com/microformats/php-mf2/pull/194))
* Refactor implied name code to match new implied name handling of photo and url ([#193](https://github.com/microformats/php-mf2/pull/193))
* Moved this repo to the microformats GitHub organization ([#179](https://github.com/microformats/php-mf2/issues/179))

#### v0.4.5

2018-08-02

Bugfixes:

* Fix for parsing empty `e-` elements

Other Updates:

* Added `.editorconfig` to the project and cleaned up whitespace across all files

#### v0.4.4

2018-08-01

Bugfixes:

* Ensure empty `properties` is an object `{}` rather than array  `[]` ([#171](https://github.com/microformats/php-mf2/issues/171))
* Ensure the parser does not mutate the DOMDOcument passed in ([#174](https://github.com/microformats/php-mf2/issues/174))
* Fix for multiple class names in backcompat parsing ([#156](https://github.com/microformats/php-mf2/issues/156))

Microformats Parsing Updates:

* New algorithm for plaintext values ([#168](https://github.com/microformats/php-mf2/pull/168) and [parsing issue #15](https://github.com/microformats/microformats2-parsing/issues/15))
* Always resolve URLs from `u-` properties even when not from a link element ([Parsing issue #10](https://github.com/microformats/microformats2-parsing/issues/10))

Other Updates:

* Improved test coverage

#### v0.4.3

2018-03-29

If the [masterminds/html5](https://github.com/Masterminds/html5-php) HTML5 parser is available, the Mf2 parser will use that instead of the built-in HTML parser. This enables proper handling of HTML5 elements such as `<article>`.

To include the HTML5 parser in your project, run:

```
composer require masterminds/html5
```

#### v0.4.2

2018-03-29

Fixes:

* [#165](https://github.com/microformats/php-mf2/pull/165) - Prevents inadvertently adding whitespace to the html value
* [#158](https://github.com/microformats/php-mf2/issues/158) - Allows numbers in vendor prefixed names
* [#160](https://github.com/microformats/php-mf2/issues/160) - Ignores class names with consecutive dashes
* [#159](https://github.com/microformats/php-mf2/issues/159) - Remove duplicate values from type and rels arrays
* [#162](https://github.com/microformats/php-mf2/pull/162) - Improved rel attribute parsing

Backcompat:

* [#157](https://github.com/microformats/php-mf2/issues/157) - Parse `rel=tag` as `p-category` for hEntry and hReview

#### v0.4.1

2018-03-15

Fixes:

* [#153](https://github.com/microformats/php-mf2/issues/153) - Fixes parsed timestamps authored with a Z timezone offset
* [#151](https://github.com/microformats/php-mf2/issues/151) - Adds back "value" of nested microformats when no matching property exists

#### v0.4.0

2018-03-13

Breaking changes:

* [#125](https://github.com/microformats/php-mf2/pull/125) - Add `rel-urls` to parsed result. Removes `alternates` by default but still available behind a feature flag.
* [#142](https://github.com/microformats/php-mf2/pull/142) - Reduce instances of implied `p-name`. See Microformats issue [#6](https://github.com/microformats/microformats2-parsing/issues/6). This means it is now possible for the parsed result to *not* have a `name` property, whereas before there was always a `name` property on an object. Make sure consuming code can handle an object without a name now.

Fixes:

* [#124](https://github.com/microformats/php-mf2/pull/124) - Fix for experimental lang parsing
* [#127](https://github.com/microformats/php-mf2/issues/127) - Fix for parsing `h-*` class names containing invalid characters.
* [#131](https://github.com/microformats/php-mf2/pull/131) - Improved `dt-` parsing. Issues [#126](https://github.com/microformats/php-mf2/issues/126) and [#115](https://github.com/microformats/php-mf2/issues/115).
* [#130](https://github.com/microformats/php-mf2/issues/130) - Fix for implied properties with empty attributes.
* [#135](https://github.com/microformats/php-mf2/issues/135) - Trim leading and tailing whitespace from HTML value as well as text value.
* [#137](https://github.com/microformats/php-mf2/issues/137) - Fix backcompat hfeed parsing.
* [#134](https://github.com/microformats/php-mf2/issues/134) - Fix `rel=bookmark` backcompat parsing.
* [#116](https://github.com/microformats/php-mf2/issues/116) - Fix backcompat parsing for `summary` property in `hreview`
* [#149](https://github.com/microformats/php-mf2/issues/149) - Fix for datetime parsing, no longer tries to interpret the value and passes through instead

#### v0.3.2

2017-05-27

* Fixed how the Microformats tests repo is loaded via composer
* Moved experimental language parsing feature behind an opt-in flag
* [#121](https://github.com/microformats/php-mf2/pull/121) Fixed language detection to support parsing of HTML fragments

#### v0.3.1

2017-05-24

* [#89](https://github.com/microformats/php-mf2/issues/89) - Fixed parsing empty `img alt=""` attributes
* [#91](https://github.com/microformats/php-mf2/issues/91) - Ignore rel values from HTML tags that don't allow rel values
* [#57](https://github.com/microformats/php-mf2/issues/57) - Implement hAtom rel=bookmark backcompat
* [#94](https://github.com/microformats/php-mf2/pull/94) - Fixed HTML output when parsing e-* properties
* [#97](https://github.com/microformats/php-mf2/pull/97) - Experimental language parsing
* [#88](https://github.com/microformats/php-mf2/issues/88) - Fix for implied photo parsing
* [#102](https://github.com/microformats/php-mf2/pull/102) - Ignore classes with numbers or capital letters
* [#111](https://github.com/microformats/php-mf2/pull/111) - Improved backcompat parsing
* [#106](https://github.com/microformats/php-mf2/issues/106) - Send `Accept: text/html` header when using the `fetch` method
* [#114](https://github.com/microformats/php-mf2/issues/114) - Parse `poster` attribute for `video` tags
* [#118](https://github.com/microformats/php-mf2/issues/118) - Fixes parsing elements with missing attributes
* Tests now use [microformats/tests](https://github.com/microformats/tests) repo

Many thanks to @gRegorLove for the major overhaul of the backcompat parsing!

#### v0.3.0

2016-03-14

* Requires PHP 5.4 at minimum (PHP 5.3 is EOL)
* Licensed under CC0 rather than MIT
* Merges Pull requests #70, #73, #74, #75, #77, #80, #82, #83, #85 and #86.
* Variety of small bug fixes and features including improved whitespace support, removal of style and script contents from plaintext properties
* All PHPUnit tests passing finally

Many thanks to @aaronpk, @diplix, @dissolve, @dymcx @gRegorLove, @jeena, @veganstraightedge and @voxpelli for all your hard work opening issues and sending and merging PRs!

#### v0.2.12

2015-07-12

* Merges pull requests [#65](https://github.com/microformats/php-mf2/pull/65), [#66](https://github.com/microformats/php-mf2/pull/66) and [#67](https://github.com/microformats/php-mf2/pull/67).
* Fixes issue [#64](https://github.com/microformats/php-mf2/issues/64).

Many thanks to @aaronpk, @gRegorLove and @kylewm for contributions, @aaronpk and @kevinmarks for PR management and @tantek for issue reporting!

#### v0.2.11

2015-07-10

#### v0.2.10

2015-04-29

* Merged [#58](https://github.com/microformats/php-mf2/pull/58), fixing some parsing bugs and adding support for area element parsing. Thanks so much for your hard work and patience, <a class="h-card" href="http://ben.thatmustbe.me/">Ben</a>!

#### v0.2.9

2014-08-06

* Added backcompat classmap for hProduct, associated tests
* Started GPG signing version tags as barnaby@waterpigs.co.uk, fingerprint CBC7 7876 BF7C 9637 B6AE 77BA 7D49 834B 0416 CFA3

#### v0.2.8

2014-07-17

* Fixed issue #51 causing php-mf2 to not work with PHP 5.3
* Fixed issue #52 correctly handling the `<template>` element by ignoring it
* Fixed issue #53 improving the plaintext parsing of `<img>` elements

#### v0.2.7

2014-06-18

* Added `Mf2\fetch()` which fetches content from a URL and returns parsed microformats
* Added implied `dt-end` discovery (thanks for all your hard work, @gRegorLove!)
* Fixed issue causing classnames like `blah e- blah` to produce properties with numeric keys (thanks @aaronpk and @gRegorLove)
* Fixed issue causing resolved URLs to not include port numbers (thanks @aaronpk)

#### v0.2.6

* Added JSON mode as long-term fix for #29
* Fixed bug causing microformats nested under multiple property names to be parsed only once

#### v0.2.5

* Removed conditional replacing empty rel list with stdclass. Original purpose was to make JSON-encoding the output from the parser correct but it also caused Fatal Errors due to trying to treat stdclass as array.

#### v0.2.4

#### v0.2.3

* Made p-* parsing consistent with implied name parsing
* Stopped collapsing whitespace in p-* properties
* Implemented unicodeTrim which removes   characters as well as regex \s
* Added support for implied name via abbr[title]
* Prevented excessively nested value-class elements from being parsed incorrectly, removed incorrect separator which was getting added in some cases
* Updated u-* parsing to be spec-compliant, matching [href] before value-class and only attempting URL resolution for URL attributes
* Added support for input[value] parsing
* Tests for all the above

#### v0.2.2

* Made resolveUrl method public, allowing advanced parsers and subclasses to make use of it
* Fixed bug causing multiple duplicate property values to appear

#### v0.2.1

* Fixed bug causing classic microformats property classnames to not be parsed correctly

#### v0.2.0 (BREAKING CHANGES)

* Namespace change from mf2 to Mf2, for PSR-0 compatibility
* `Mf2\parse()` function added to simplify the most common case of just parsing some HTML
* Updated e-* property parsing rules to match mf2 parsing spec — instead of producing inconsistent HTML content, it now produces dictionaries like <pre><code>
{
	"html": "<b>The Content</b>",
	"value: "The Content"
}
</code></pre>
* Removed `htmlSafe` options as new e-* parsing rules make them redundant
* Moved a whole load of static functions out of the class and into standalone functions
* Changed autoloading to always include Parser.php instead of using classmap

#### v0.1.23

* Made some changes to the way back-compatibility with classic microformats are handled, ignoring classic property classnames inside mf2 roots and outside classic roots
* Deprecated ability to add new classmaps, removed twitter classmap. Use [php-mf2-shim](http://github.com/microformats/php-mf2-shim) instead, it’s better

#### v0.1.22

* Converts classic microformats by default

#### v0.1.21

* Removed webignition dependency, also removing ext-intl dependency. php-mf2 is now a standalone, single file library again
* Replaced webignition URL resolving with custom code passing almost all tests, courtesy of <a class="h-card" href="http://aaronparecki.com">Aaron Parecki</a>

#### v0.1.20

* Added in almost-perfect custom URL resolving code

#### v0.1.19 (2013-06-11)

* Required stable version of webigniton/absolute-url-resolver, hopefully resolving versioning problems

#### v0.1.18 (2013-06-05)

* Fixed problems with isElementParsed, causing elements to be incorrectly parsed
* Cleaned up some test files

#### v0.1.17

* Rewrote some PHP 5.4 array syntax which crept into 0.1.16 so php-mf2 still works on PHP 5.3
* Fixed a bug causing weird partial microformats to be added to parent microformats if they had doubly property-nested children
* Finally actually licensed this project under a real license (MIT, in composer.json)
* Suggested barnabywalters/mf-cleaner in composer.json

#### v0.1.16

* Ability to parse from only an ID
* Context DOMElement can be passed to $parse
* Parser::query runs XPath queries on the current document
* When parsing e-* properties, elements with @src, @data or @href have relative URLs resolved in the output

#### v0.1.15

* Added html-safe options
* Added rel+rel-alternate parsing

## License

php-mf2 is dedicated to the public domain using Creative Commons -- CC0 1.0 Universal.

http://creativecommons.org/publicdomain/zero/1.0

repo_name: ksuudilla/gnu

license: mit


code:
#!/usr/bin/env php
<?php

$files = array(
  __DIR__ . '/../vendor/autoload.php',
  __DIR__ . '/../../../autoload.php',
  __DIR__ . '/../Mf2/Parser.php'
);

foreach ($files as $file) {
	if (file_exists($file)) {
		require $file;
		define('COMPOSER_INSTALL_LOCATION', $file);
		break;
	}
}

if (!defined('COMPOSER_INSTALL_LOCATION')) {
	die('A composer /vendor/autoload.php file could not be found. You need to install composer: https://getcomposer.org/download');
}

$url = $argv[1];

if (empty($url)) {
	die("Please provide a URL to fetch+parse, e.g. bin/fetch-mf2 waterpigs.co.uk/notes");
}

if (!file_exists($url)) {
	if (!preg_match('|https?://|', $url)) {
		$url = 'http://' . $url;
	}
}

$result = Mf2\fetch($url);

if (defined('JSON_PRETTY_PRINT')) {
	echo json_encode($result, JSON_PRETTY_PRINT);
} else {
	echo json_encode($result);
}


code:
#!/usr/bin/env php
<?php

$files = array(
  __DIR__ . '/../vendor/autoload.php',
  __DIR__ . '/../../../autoload.php',
  __DIR__ . '/../Mf2/Parser.php'
);

foreach ($files as $file) {
	if (file_exists($file)) {
		require $file;
		define('COMPOSER_INSTALL_LOCATION', $file);
		break;
	}
}

if (!defined('COMPOSER_INSTALL_LOCATION')) {
	die('A composer /vendor/autoload.php file could not be found. You need to install composer: https://getcomposer.org/download');
}

if (!empty($argv[1])) {
	$url = $argv[1];
	if (!preg_match('|https?://|', $url)) {
		$url = 'http://' . $url;
	}
} else {
	$url = null;
}

$result = Mf2\parse(fgets(STDIN), $url);

if (defined('JSON_PRETTY_PRINT')) {
	echo json_encode($result, JSON_PRETTY_PRINT);
} else {
	echo json_encode($result);
}


type: library

description: A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API

keywords:
microformats
microformats 2
parser
semantic
html

barnabywalters/mf-cleaner: To more easily handle the canonical data php-mf2 gives you

masterminds/html5: Alternative HTML parser for PHP, for better HTML5 support.