Merge remote-tracking branch 'upstream/master'
[quix0rs-gnu-social.git] / extlib / Mf2 / Parser.php
index 27805f2324e54b0802e7f6a18d053be15ac98bff..b8a954f2c8631f965c7880b45ec5c2e67f29da0e 100644 (file)
@@ -13,17 +13,17 @@ use stdClass;
 
 /**
  * Parse Microformats2
- * 
+ *
  * Functional shortcut for the commonest cases of parsing microformats2 from HTML.
- * 
+ *
  * Example usage:
- * 
+ *
  *     use Mf2;
  *     $output = Mf2\parse('<span class="h-card">Barnaby Walters</span>');
  *     echo json_encode($output, JSON_PRETTY_PRINT);
- * 
+ *
  * Produces:
- * 
+ *
  *     {
  *      "items": [
  *       {
@@ -35,7 +35,7 @@ use stdClass;
  *      ],
  *      "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
@@ -84,7 +84,7 @@ function fetch($url, $convertClassic = true, &$curlInfo=null) {
 /**
  * Unicode to HTML Entities
  * @param string $input String containing characters to convert into HTML entities
- * @return string 
+ * @return string
  */
 function unicodeToHtmlEntities($input) {
        return mb_convert_encoding($input, 'HTML-ENTITIES', mb_detect_encoding($input));
@@ -92,10 +92,10 @@ function unicodeToHtmlEntities($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
@@ -113,10 +113,10 @@ function unicodeTrim($str) {
 
 /**
  * Microformat Name From Class string
- * 
- * Given the value of @class, get the relevant mf classnames (e.g. h-card, 
+ *
+ * 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
@@ -127,9 +127,9 @@ function mfNamesFromClass($class, $prefix='h-') {
        $matches = array();
 
        foreach ($classes as $classname) {
-               $compare_classname = strtolower(' ' . $classname);
-               $compare_prefix = strtolower(' ' . $prefix);
-               if (stristr($compare_classname, $compare_prefix) !== false && ($compare_classname != $compare_prefix)) {
+               $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));
                }
        }
@@ -139,10 +139,10 @@ function mfNamesFromClass($class, $prefix='h-') {
 
 /**
  * Get Nested µf Property Name From Class
- * 
- * Returns all the p-, u-, dt- or e- prefixed classnames it finds in a 
+ *
+ * Returns all the p-, u-, dt- or e- prefixed classnames it finds in a
  * space-separated string.
- * 
+ *
  * @param string $class
  * @return array
  */
@@ -153,19 +153,24 @@ function nestedMfPropertyNamesFromClass($class) {
        $class = str_replace(array(' ', '       ', "\n"), ' ', $class);
        foreach (explode(' ', $class) as $classname) {
                foreach ($prefixes as $prefix) {
-                       $compare_classname = strtolower(' ' . $classname);
-                       if (stristr($compare_classname, $prefix) && ($compare_classname != $prefix)) {
-                               $propertyNames = array_merge($propertyNames, mfNamesFromClass($classname, ltrim($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()
@@ -192,28 +197,27 @@ 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 specified
+       // If no am/pm is specified:
        if (empty($matches[4])) {
                return $time;
-       }
-       // else am/pm specified
-       else {
+       } else {
+               // Otherwise, am/pm is specified.
                $meridiem = strtolower(str_replace('.', '', $matches[4]));
 
-               // hours
+               // Hours.
                $hh = $matches[1];
 
-               // add 12 to the pm hours
+               // Add 12 to hours if pm applies.
                if ($meridiem == 'pm' && ($hh < 12)) {
                        $hh += 12;
                }
 
                $hh = str_pad($hh, 2, '0', STR_PAD_LEFT);
 
-               // minutes
+               // Minutes.
                $mm = (empty($matches[2]) ) ? '00' : $matches[2];
 
-               // seconds, only if supplied
+               // Seconds, only if supplied.
                if (!empty($matches[3])) {
                        $ss = $matches[3];
                }
@@ -229,11 +233,11 @@ function convertTimeFormat($time) {
 
 /**
  * Microformats2 Parser
- * 
+ *
  * A class which holds state for parsing microformats2 from HTML.
- * 
+ *
  * Example usage:
- * 
+ *
  *     use Mf2;
  *     $parser = new Mf2\Parser('<p class="h-card">Barnaby Walters</p>');
  *     $output = $parser->parse();
@@ -244,18 +248,18 @@ class Parser {
 
        /** @var DOMXPath object which can be used to query over any fragment*/
        public $xpath;
-       
+
        /** @var DOMDocument */
        public $doc;
-       
+
        /** @var SplObjectStorage */
        protected $parsed;
-       
+
        public $jsonMode;
 
        /**
         * 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.
@@ -271,20 +275,20 @@ class Parser {
                        $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;
@@ -296,31 +300,31 @@ class Parser {
                foreach ($this->xpath->query('//template') as $templateEl) {
                        $templateEl->parentNode->removeChild($templateEl);
                }
-               
+
                $this->baseurl = $baseurl;
                $this->doc = $doc;
                $this->parsed = 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;
        }
-       
+
        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;
        }
 
@@ -352,72 +356,72 @@ class Parser {
 
        // 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 
+               // 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;
-               
+
                $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 
+        * 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 it’s 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;
-               
+
                if ($p->tagName == 'img' and $p->getAttribute('alt') !== '') {
                        $pValue = $p->getAttribute('alt');
                } elseif ($p->tagName == 'area' and $p->getAttribute('alt') !== '') {
@@ -429,13 +433,13 @@ class Parser {
                } else {
                        $pValue = unicodeTrim($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
@@ -443,18 +447,18 @@ class Parser {
        public function parseU(\DOMElement $u) {
                if (($u->tagName == 'a' or $u->tagName == 'area') and $u->getAttribute('href') !== null) {
                        $uValue = $u->getAttribute('href');
-               } elseif ($u->tagName == 'img' and $u->getAttribute('src') !== null) {
+               } elseif (in_array($u->tagName, array('img', 'audio', 'video', 'source')) and $u->getAttribute('src') !== null) {
                        $uValue = $u->getAttribute('src');
                } elseif ($u->tagName == 'object' and $u->getAttribute('data') !== null) {
                        $uValue = $u->getAttribute('data');
                }
-               
+
                if (isset($uValue)) {
                        return $this->resolveUrl($uValue);
                }
-               
+
                $classTitle = $this->parseValueClassTitle($u);
-               
+
                if ($classTitle !== null) {
                        return $classTitle;
                } elseif ($u->tagName == 'abbr' and $u->getAttribute('title') !== null) {
@@ -468,7 +472,7 @@ class Parser {
 
        /**
         * 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
         * @return string The datetime string found
@@ -477,11 +481,11 @@ class Parser {
                // 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');
@@ -591,16 +595,16 @@ class Parser {
                                $dtValue = $dt->nodeValue;
                        }
 
-                       if ( preg_match('/(\d{4}-\d{2}-\d{2})/', $dtValue, $matches) ) {
+                       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 recnetly parsed dt- value
+                * 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{1,2}(Z?[+|-]\d{2}:?\d{2})?/', $dtValue) or preg_match('/^\d{1,2}[a|p]m/', $dtValue)) && !empty($dates) ) {
+               if ((preg_match('/^\d{1,2}:\d{1,2}(Z?[+|-]\d{2}:?\d{2})?/', $dtValue) or preg_match('/^\d{1,2}[a|p]m/', $dtValue)) && !empty($dates)) {
                        $dtValue = convertTimeFormat($dtValue);
                        $dtValue = end($dates) . 'T' . unicodeTrim($dtValue, 'T');
                }
@@ -613,15 +617,15 @@ class Parser {
         *
         *      @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);
@@ -630,7 +634,7 @@ class Parser {
                foreach ($e->childNodes as $node) {
                        $html .= $node->C14N();
                }
-               
+
                return array(
                        'html' => $html,
                        'value' => unicodeTrim($this->textContent($e))
@@ -639,7 +643,7 @@ class Parser {
 
        /**
         * Recursively parse microformats
-        * 
+        *
         * @param DOMElement $e The element to parse
         * @return array A representation of the values contained within microformat $e
         */
@@ -660,26 +664,39 @@ class Parser {
                foreach ($this->xpath->query('.//*[contains(concat(" ", @class)," h-")]', $e) as $subMF) {
                        // Parse
                        $result = $this->parseH($subMF);
-                       
+
                        // If result was already parsed, skip it
                        if (null === $result)
                                continue;
                        
+                       // In most cases, the value attribute of the nested microformat should be the p- parsed value of the elemnt.
+                       // The only times this is different is when the microformat is nested under certain prefixes, which are handled below.
                        $result['value'] = $this->parseP($subMF);
 
                        // Does this µf have any property names other than h-*?
                        $properties = nestedMfPropertyNamesFromElement($subMF);
-                       
+
                        if (!empty($properties)) {
                                // Yes! It’s a nested property µf
-                               foreach ($properties as $property) {
-                                       $return[$property][] = $result;
+                               foreach ($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'] = $prefixSpecificResult['properties']['name'][0];
+                                       } elseif (in_array('e-', $prefixes)) {
+                                               $eParsedResult = $this->parseE($subMF);
+                                               $prefixSpecificResult['html'] = $eParsedResult['html'];
+                                               $prefixSpecificResult['value'] = $eParsedResult['value'];
+                                       } elseif (in_array('u-', $prefixes)) {
+                                               $prefixSpecificResult['value'] = $this->parseU($subMF);
+                                       }
+                                       $return[$property][] = $prefixSpecificResult;
                                }
                        } else {
                                // No, it’s a child µf
                                $children[] = $result;
                        }
-                       
+
                        // Make sure this sub-mf won’t get parsed as a µf or property
                        // TODO: Determine if clearing this is required?
                        $this->elementPrefixParsed($subMF, 'h');
@@ -689,19 +706,24 @@ class Parser {
                        $this->elementPrefixParsed($subMF, 'e');
                }
 
+               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) {
                        if ($this->isElementParsed($p, 'p'))
                                continue;
 
                        $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');
                }
@@ -710,32 +732,32 @@ class Parser {
                foreach ($this->xpath->query('.//*[contains(concat(" ",  @class)," u-")]', $e) as $u) {
                        if ($this->isElementParsed($u, 'u'))
                                continue;
-                       
+
                        $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');
                }
-               
+
                // Handle dt-*
                foreach ($this->xpath->query('.//*[contains(concat(" ", @class), " dt-")]', $e) as $dt) {
                        if ($this->isElementParsed($dt, 'dt'))
                                continue;
-                       
+
                        $dtValue = $this->parseDT($dt, $dates);
-                       
+
                        if ($dtValue) {
                                // Add the value to the array for dt- properties
                                foreach (mfNamesFromElement($dt, 'dt-') as $propName) {
                                        $return[$propName][] = $dtValue;
                                }
                        }
-                       
+
                        // Make sure this sub-mf won’t get parsed as a top level mf
                        $this->elementPrefixParsed($dt, 'dt');
                }
@@ -762,22 +784,43 @@ class Parser {
                if (!array_key_exists('name', $return)) {
                        try {
                                // Look for img @alt
-                               if ($e->tagName == 'img' and $e->getAttribute('alt') != '')
+                               if (($e->tagName == 'img' or $e->tagName == 'area') and $e->getAttribute('alt') != '')
                                        throw new Exception($e->getAttribute('alt'));
-                               
+
                                if ($e->tagName == 'abbr' and $e->hasAttribute('title'))
                                        throw new Exception($e->getAttribute('title'));
-                               
+
                                // Look for nested img @alt
                                foreach ($this->xpath->query('./img[count(preceding-sibling::*)+count(following-sibling::*)=0]', $e) as $em) {
-                                       if ($em->getAttribute('alt') != '')
+                                       $emNames = mfNamesFromElement($em, 'h-');
+                                       if (empty($emNames) && $em->getAttribute('alt') != '') {
                                                throw new Exception($em->getAttribute('alt'));
+                                       }
+                               }
+
+                               // Look for nested area @alt
+                               foreach ($this->xpath->query('./area[count(preceding-sibling::*)+count(following-sibling::*)=0]', $e) as $em) {
+                                       $emNames = mfNamesFromElement($em, 'h-');
+                                       if (empty($emNames) && $em->getAttribute('alt') != '') {
+                                               throw new Exception($em->getAttribute('alt'));
+                                       }
                                }
 
+
                                // Look for double nested img @alt
                                foreach ($this->xpath->query('./*[count(preceding-sibling::*)+count(following-sibling::*)=0]/img[count(preceding-sibling::*)+count(following-sibling::*)=0]', $e) as $em) {
-                                       if ($em->getAttribute('alt') != '')
+                                       $emNames = mfNamesFromElement($em, 'h-');
+                                       if (empty($emNames) && $em->getAttribute('alt') != '') {
+                                               throw new Exception($em->getAttribute('alt'));
+                                       }
+                               }
+
+                               // Look for double nested img @alt
+                               foreach ($this->xpath->query('./*[count(preceding-sibling::*)+count(following-sibling::*)=0]/area[count(preceding-sibling::*)+count(following-sibling::*)=0]', $e) as $em) {
+                                       $emNames = mfNamesFromElement($em, 'h-');
+                                       if (empty($emNames) && $em->getAttribute('alt') != '') {
                                                throw new Exception($em->getAttribute('alt'));
+                                       }
                                }
 
                                throw new Exception($e->nodeValue);
@@ -812,36 +855,58 @@ class Parser {
                // Check for u-url
                if (!array_key_exists('url', $return)) {
                        // Look for img @src
-                       if ($e->tagName == 'a')
+                       if ($e->tagName == 'a' or $e->tagName == 'area')
                                $url = $e->getAttribute('href');
-                       
-                       // Look for nested img @src
+
+                       // Look for nested a @href
                        foreach ($this->xpath->query('./a[count(preceding-sibling::a)+count(following-sibling::a)=0]', $e) as $em) {
-                               $url = $em->getAttribute('href');
-                               break;
+                               $emNames = mfNamesFromElement($em, 'h-');
+                               if (empty($emNames)) {
+                                       $url = $em->getAttribute('href');
+                                       break;
+                               }
                        }
-                       
+
+                       // Look for nested area @src
+                       foreach ($this->xpath->query('./area[count(preceding-sibling::area)+count(following-sibling::area)=0]', $e) as $em) {
+                               $emNames = mfNamesFromElement($em, 'h-');
+                               if (empty($emNames)) {
+                                       $url = $em->getAttribute('href');
+                                       break;
+                               }
+                       }
+
                        if (!empty($url))
                                $return['url'][] = $this->resolveUrl($url);
                }
 
                // Make sure things are in alphabetical order
                sort($mfTypes);
-               
+
                // Phew. Return the final result.
                $parsed = array(
                        'type' => $mfTypes,
                        'properties' => $return
                );
-               if (!empty($children))
+
+               if (!empty($shape)) {
+                       $parsed['shape'] = $shape;
+               }
+
+               if (!empty($coords)) {
+                       $parsed['coords'] = $coords;
+               }
+
+               if (!empty($children)) {
                        $parsed['children'] = array_values(array_filter($children));
+               }
                return $parsed;
        }
-       
+
        /**
         * Parse Rels and Alternatives
-        * 
-        * Returns [$rels, $alternatives]. If the $rels value is to be empty, i.e. there are no links on the page 
+        *
+        * Returns [$rels, $alternatives]. If the $rels value is to be empty, i.e. there are no links on the page
         * with a rel value *not* containing `alternate`, then the type of $rels depends on $this->jsonMode. If set
         * to true, it will be a stdClass instance, optimising for JSON serialisation. Otherwise (the default case),
         * it will be an empty array.
@@ -849,18 +914,18 @@ class Parser {
        public function parseRelsAndAlternates() {
                $rels = array();
                $alternates = array();
-               
+
                // Iterate through all a, area and link elements with rel attributes
                foreach ($this->xpath->query('//*[@rel and @href]') as $hyperlink) {
                        if ($hyperlink->getAttribute('rel') == '')
                                continue;
-                       
+
                        // Resolve the href
                        $href = $this->resolveUrl($hyperlink->getAttribute('href'));
-                       
+
                        // Split up the rel into space-separated values
                        $linkRels = array_filter(explode(' ', $hyperlink->getAttribute('rel')));
-                       
+
                        // If alternate in rels, create alternate structure, append
                        if (in_array('alternate', $linkRels)) {
                                $alt = array(
@@ -869,10 +934,19 @@ class Parser {
                                );
                                if ($hyperlink->hasAttribute('media'))
                                        $alt['media'] = $hyperlink->getAttribute('media');
-                               
+
                                if ($hyperlink->hasAttribute('hreflang'))
                                        $alt['hreflang'] = $hyperlink->getAttribute('hreflang');
-                               
+
+                               if ($hyperlink->hasAttribute('title'))
+                                       $alt['title'] = $hyperlink->getAttribute('title');
+
+                               if ($hyperlink->hasAttribute('type'))
+                                       $alt['type'] = $hyperlink->getAttribute('type');
+
+                               if ($hyperlink->nodeValue)
+                                       $alt['text'] = $hyperlink->nodeValue;
+
                                $alternates[] = $alt;
                        } else {
                                foreach ($linkRels as $rel) {
@@ -880,38 +954,38 @@ class Parser {
                                }
                        }
                }
-               
+
                if (empty($rels) and $this->jsonMode) {
                        $rels = new stdClass();
                }
-               
+
                return array($rels, $alternates);
        }
-       
+
        /**
         * Kicks off the parsing routine
-        * 
+        *
         * If `$htmlSafe` is set, any angle brackets in the results from non e-* properties
         * will be HTML-encoded, bringing all output to the same level of encoding.
-        * 
+        *
         * If a DOMElement is set as the $context, only descendants of that element will
         * be parsed for microformats.
-        * 
+        *
         * @param bool $htmlSafe whether or not to html-encode non e-* properties. Defaults to false
         * @param DOMElement $context optionally an element from which to parse microformats
         * @return array An array containing all the µfs found in the current document
         */
        public function parse($convertClassic = true, DOMElement $context = null) {
                $mfs = array();
-               
+
                if ($convertClassic) {
                        $this->convertLegacy();
                }
-               
+
                $mfElements = null === $context
                        ? $this->xpath->query('//*[contains(concat(" ", @class), " h-")]')
                        : $this->xpath->query('.//*[contains(concat(" ",        @class), " h-")]', $context);
-               
+
                // Parser microformats
                foreach ($mfElements as $node) {
                        // For each microformat
@@ -920,64 +994,64 @@ class Parser {
                        // Add the value to the array for this property type
                        $mfs[] = $result;
                }
-               
+
                // Parse rels
                list($rels, $alternates) = $this->parseRelsAndAlternates();
-               
+
                $top = array(
                        'items' => array_values(array_filter($mfs)),
                        'rels' => $rels
                );
-               
+
                if (count($alternates))
                        $top['alternates'] = $alternates;
-               
+
                return $top;
        }
-       
+
        /**
         * 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 
+        *
+        * 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));
        }
 
        /**
         * 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 => $new) {
@@ -986,16 +1060,16 @@ class Parser {
                                }
                        }
                }
-               
+
                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
@@ -1003,7 +1077,7 @@ class Parser {
        public function query($expression, $context = null) {
                return $this->xpath->query($expression, $context);
        }
-       
+
        /**
         * Classic Root Classname map
         */
@@ -1013,11 +1087,11 @@ class Parser {
                'hentry' => 'h-entry',
                'hrecipe' => 'h-recipe',
                'hresume' => 'h-resume',
-               'hevent' => 'h-event',
+               'vevent' => 'h-event',
                'hreview' => 'h-review',
                'hproduct' => 'h-product'
        );
-       
+
        public $classicPropertyMap = array(
                'vcard' => array(
                        'fn' => 'p-name',
@@ -1084,7 +1158,7 @@ class Parser {
                        'skill' => 'p-skill',
                        'affiliation' => 'p-affiliation h-card',
                ),
-               'hevent' => array(
+               'vevent' => array(
                        'dtstart' => 'dt-start',
                        'dtend' => 'dt-end',
                        'duration' => 'dt-duration',
@@ -1246,7 +1320,7 @@ function resolveUrl($baseURI, $referenceURI) {
 # 5.2.3 Merge Paths
 function mergePaths($base, $reference) {
        # If the base URI has a defined authority component and an empty
-       #    path, 
+       #    path,
        if($base['authority'] && $base['path'] == null) {
                # then return a string consisting of "/" concatenated with the
                # reference's path; otherwise,