Repository: microformats/php-mf2 (php)

Issue:
Fix parsing class properties with newlines
Tracked this one down via https://github.com/indieweb/indiewebify-me/issues/102. Initially I thought that issue was due to the php-mf2 version (it did need to be updated), but I realized once I parsed with php-mf2 0.5.0, the `p-name` was not being found when there's new lines between the classes.

I tried a several variations based on that source HTML and came up with some minimal tests:

```html
<div class="h-entry">
<h1 class="post_title__text
p-name">Page Title</h1>
<p class="p-summary">A summary so the p-name won't be implied. This test demonstrates p-name is not being parsed.</p>
</div>
```

Parsed result [with 0.5.0](http://php.microformats.io/?id=20221008222422564):

```json
{
    "items": [
        {
            "type": [
                "h-entry"
            ],
            "properties": {
                "summary": [
                    "A summary so the p-name won't be implied. This test demonstrates p-name is not being parsed."
                ]
            }
        }
    ],
    "rels": {},
    "rel-urls": {},
    "debug": {
        "package": "https://packagist.org/packages/mf2/mf2",
        "source": "https://github.com/indieweb/php-mf2",
        "version": "v0.5.0",
        "note": [
            "This output was generated from the php-mf2 library available at https://github.com/indieweb/php-mf2",
            "Please file any issues with the parser at https://github.com/indieweb/php-mf2/issues",
            "Using the Masterminds HTML5 parser"
        ]
    }
}
```

And with implied p-name parsing:

```html
<div class="h-entry">
<h1 class="post_title__text
p-name">Page Title</h1>
<p> Here is some content without microformats. If the parser does not find p-name above, it will try to imply p-name, which will include this text as well. </p>
<p> implied p-name parsing will occur if the parser doesn't find any p-* or e-* properties. </p>
</div>
```

Parsed result with [php-mf2 0.5.0](http://php.microformats.io/?id=20221008221625954)

```json
{
    "items": [
        {
            "type": [
                "h-entry"
            ],
            "properties": {
                "name": [
                    "Page Title\nHere is some content without microformats. If the parser does not find p-name above, it will try to imply p-name, which will include this text as well.\nimplied p-name parsing will occur if the parser doesn't find any p-* or e-* properties."
                ]
            }
        }
    ],
    "rels": {},
    "rel-urls": {},
    "debug": {
        "package": "https://packagist.org/packages/mf2/mf2",
        "source": "https://github.com/indieweb/php-mf2",
        "version": "v0.5.0",
        "note": [
            "This output was generated from the php-mf2 library available at https://github.com/indieweb/php-mf2",
            "Please file any issues with the parser at https://github.com/indieweb/php-mf2/issues",
            "Using the Masterminds HTML5 parser"
        ]
    }
}
```

Pull request description:
Apply space normalization in all XPath searches for classes
This fixes #245, though it doesn't particularly prevent the problem from cropping up again in future queries.

Patch:
diff --git a/Mf2/Parser.php b/Mf2/Parser.php
index 8b6485b..7ac471e 100644
--- a/Mf2/Parser.php
+++ b/Mf2/Parser.php
@@ -602,7 +602,7 @@ class Parser {
 	 * @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);
+		$valueClassElements = $this->xpath->query('./*[contains(concat(" ", normalize-space(@class), " "), " value ")]', $e);
 
 		if ($valueClassElements->length !== 0) {
 			// Process value-class stuff
@@ -614,7 +614,7 @@ class Parser {
 			return unicodeTrim($val);
 		}
 
-		$valueTitleElements = $this->xpath->query('./*[contains(concat(" ", @class, " "), " value-title ")]', $e);
+		$valueTitleElements = $this->xpath->query('./*[contains(concat(" ", normalize-space(@class), " "), " value-title ")]', $e);
 
 		if ($valueTitleElements->length !== 0) {
 			// Process value-title stuff
@@ -702,7 +702,7 @@ class Parser {
 	 */
 	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);
+		$valueClassChildren = $this->xpath->query('./*[contains(concat(" ", normalize-space(@class), " "), " value ") or contains(concat(" ", normalize-space(@class), " "), " value-title ")]', $dt);
 		$dtValue = false;
 
 		if ($valueClassChildren->length > 0) {
@@ -978,7 +978,7 @@ class Parser {
 		}
 
 		// Handle p-*
-		foreach ($this->xpath->query('.//*[contains(concat(" ", @class) ," p-")]', $e) as $p) {
+		foreach ($this->xpath->query('.//*[contains(concat(" ", normalize-space(@class)) ," p-")]', $e) as $p) {
 			// element is already parsed
 			if ($this->isElementParsed($p, 'p')) {
 				continue;
@@ -1003,7 +1003,7 @@ class Parser {
 		}
 
 		// Handle u-*
-		foreach ($this->xpath->query('.//*[contains(concat(" ",  @class)," u-")]', $e) as $u) {
+		foreach ($this->xpath->query('.//*[contains(concat(" ", normalize-space(@class))," u-")]', $e) as $u) {
 			// element is already parsed
 			if ($this->isElementParsed($u, 'u')) {
 				continue;
@@ -1028,7 +1028,7 @@ class Parser {
 		$temp_dates = array();
 
 		// Handle dt-*
-		foreach ($this->xpath->query('.//*[contains(concat(" ", @class), " dt-")]', $e) as $dt) {
+		foreach ($this->xpath->query('.//*[contains(concat(" ", normalize-space(@class)), " dt-")]', $e) as $dt) {
 			// element is already parsed
 			if ($this->isElementParsed($dt, 'dt')) {
 				continue;
@@ -1063,7 +1063,7 @@ class Parser {
 		}
 
 		// Handle e-*
-		foreach ($this->xpath->query('.//*[contains(concat(" ", @class)," e-")]', $e) as $em) {
+		foreach ($this->xpath->query('.//*[contains(concat(" ", normalize-space(@class))," e-")]', $e) as $em) {
 			// element is already parsed
 			if ($this->isElementParsed($em, 'e')) {
 				continue;
@@ -1755,7 +1755,7 @@ class Parser {
 
 		// replace all roots
 		foreach ($this->classicRootMap as $old => $new) {
-			foreach ($xp->query('//*[contains(concat(" ", @class, " "), " ' . $old . ' ") and not(contains(concat(" ", @class, " "), " ' . $new . ' "))]') as $el) {
+			foreach ($xp->query('//*[contains(concat(" ", normalize-space(@class), " "), " ' . $old . ' ") and not(contains(concat(" ", normalize-space(@class), " "), " ' . $new . ' "))]') as $el) {
 				$el->setAttribute('class', $el->getAttribute('class') . ' ' . $new);
 			}
 		}
@@ -1763,7 +1763,7 @@ class Parser {
 		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) {
+				foreach ($xp->query('//*[contains(concat(" ", normalize-space(@class), " "), " ' . $oldRoot . ' ")]//*[contains(concat(" ", normalize-space(@class), " "), " ' . $old . ' ") and not(contains(concat(" ", normalize-space(@class), " "), " ' . $data['replace'] . ' "))]') as $el) {
 					$el->setAttribute('class', $el->getAttribute('class') . ' ' . $data['replace']);
 				}
 			}


Test patch:
diff --git a/tests/Mf2/ParserTest.php b/tests/Mf2/ParserTest.php
index a364557..dcbedb0 100644
--- a/tests/Mf2/ParserTest.php
+++ b/tests/Mf2/ParserTest.php
@@ -898,5 +898,21 @@ EOF;
 		$this->assertEquals(1, count($rootEls));
 		$this->assertEquals('h-vendor123-name', $rootEls->item(0)->getAttribute('class'));
 	}
+
+	/**
+	 * @see https://github.com/microformats/php-mf2/issues/245
+	 */
+	public function testNewlineBeforePrefix() {
+		$input = <<<EOT
+<div class="h-entry">
+<h1 class="post_title__text
+p-name">Page Title</h1>
+<p class="p-summary">A summary so the p-name won't be implied. This test demonstrates p-name is not being parsed.</p>
+</div>
+EOT;
+		$result = Mf2\parse($input);
+		$this->assertEquals('Page Title', $result['items'][0]['properties']['name'][0]);
+		$this->assertEquals('A summary so the p-name won\'t be implied. This test demonstrates p-name is not being parsed.', $result['items'][0]['properties']['summary'][0]);
+	}
 }