Repository: microformats/php-mf2 (php)

Issue:
Properties not parsed when using Tailwind height class
I managed to get a minimal example that happened to me while working with Tailwind CSS, but i guess it's more generic to any class that starts with `h-` prefix.

If i use Tailwind CSS `h-<number>` class on some elements that have other microformat classes like in this minimal example with u-logo and u-nickname:
```xml
<article class="h-card">
   <h1> TITLE </h1>
   <img class="h-36 photo u-logo" 
          alt="An example alt title"
          src="https://example.com/img.png"
    />
    <p class="h-21 u-nickname"> John Doe </p>
</article>
```
The parser skips mf properties and produces :
```json
{
    "items": [
        {
            "type": [
                "h-card"
            ],
            "properties": {
                "name": [
                    "TITLE An example alt title\nJohn Doe"
                ]
            }
        }
    ],
    "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"
        ]
    }
}
```

Instead, if I remove h-<number> classes, it's working as expected:

```xml
<article class="h-card">
   <h1> TITLE </h1>
   <img class="photo u-logo" 
          alt="An example alt title"
          src="https://example.com/img.png"
    />
    <p class="u-nickname"> John Doe </p>
</article>
```
with output:
```json
{
    "items": [
        {
            "type": [
                "h-card"
            ],
            "properties": {
                "logo": [
                    {
                        "value": "https://example.com/img.png",
                        "alt": "An example alt title"
                    }
                ],
                "nickname": [
                    "John Doe"
                ],
                "name": [
                    "TITLE An example alt title\nJohn Doe"
                ]
            }
        }
    ],
    "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"
        ]
    }
}
```

This scenario seems to work using the [Go implementation](https://go.microformats.io/)
properties with invalid names are not properly ignored
in `v0.5.0`, the following example (distilled down from https://catgirlin.space/ as of right now)

```html
<div class=" h-feed pt-2">
  <div class="p-4 h-entry">
    <a class="u-url" href="https://example.com/bar">bar </a>
  </div>
  <div class="p-4 h-entry">
    <a class="u-url" href="https://example.com/foo">foo</a>	
  </div>
</div>
```

parses as 
```json
{
    "items": [
        {
            "type": [ "h-feed"],
            "properties": [
                [
                    {
                        "type": ["h-entry" ],
                        "properties": {
                            "url": [ "https://example.com/bar"],
                            "name": [ "bar" ]
                        },
                        "value": "bar"
                    },
                    {
                        "type": [ "h-entry"],
                        "properties": {
                            "url": ["https://example.com/foo" ],
                            "name": ["foo"]
                        },
                        "value": "foo"
                    }
                ]
            ]
        }
    ],
…
```
Note how `properties` of the `h-feed`  has been turned into *two nested lists* somehow, which is completely invalid output.

Potentially related, when nesting the two h-entries, somehow a property named `0` gets generated, which is also badly wrong (note that no `0` appears in the input)

```html
<div class=" h-feed pt-2">
  <div class="p-4 h-entry">
    <a class="u-url" href="https://example.com/bar">bar </a>
    <div class="p-4 h-entry">
      <a class="u-url" href="https://example.com/foo">foo</a>	
    </div>
  <div>
</div>
```

parsed as 
```json
{
    "items": [
        {
            "type": [ "h-feed"],
            "properties": [
                [
                    {
                        "type": ["h-entry"],
                        "properties": {
                            "url": ["https://example.com/bar"],
                            "0": [
                                {
                                    "type": ["h-entry"],
                                    "properties": {
                                        "url": ["https://example.com/foo" ],
                                        "name": [ "foo" ]
                                    },
                                    "value": "foo"
                                }
                            ]
                        },
                        "value": "bar foo"
                    }
                ]
            ]
        }
    ],
```

Maybe related to #246

Pull request description:
Fix invalid property class name parsing
The `nestedMfPropertyNamesFromClass` method was parsing property classes regardless if they were valid (e.g. `p-4`). Updated the method to use existing `mfNamesFromClass` method, which uses the correct regex to skip those classes.

Also fixed a test for `nestedMfPropertyNamesFromClass` which asserted the response array was equal. The expected and actual response array had the same values, but in different order. The order is not important in that response, so I switched the assertion to `assertEqualsCanonicalizing`.

This also appeared to fix #246, so I added a test to confirm that.

Fixes #249
Fixes #246

Patch:
diff --git a/Mf2/Parser.php b/Mf2/Parser.php
index 7ac471e..48b4c88 100644
--- a/Mf2/Parser.php
+++ b/Mf2/Parser.php
@@ -166,14 +166,10 @@ 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 ($prefixes as $prefix) {
+		$classes = mfNamesFromClass($class, $prefix);
+		foreach ($classes as $property) {
+			$propertyNames[$property][] = $prefix;
 		}
 	}
 


Test patch:
diff --git a/tests/Mf2/ParserTest.php b/tests/Mf2/ParserTest.php
index dcbedb0..c2ce4b3 100644
--- a/tests/Mf2/ParserTest.php
+++ b/tests/Mf2/ParserTest.php
@@ -56,7 +56,7 @@ class ParserTest extends TestCase {
 		$test = 'someclass p-location someotherclass u-author p-author';
 		$actual = Mf2\nestedMfPropertyNamesFromClass($test);
 
-		$this->assertEquals($expected, $actual);
+		$this->assertEqualsCanonicalizing($expected, $actual);
 	}
 
 	public function testMicroformatNamesFromClassIgnoresPrefixesWithoutNames() {
@@ -914,5 +914,109 @@ EOT;
 		$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]);
 	}
+	
+	/**
+	 * @see https://github.com/microformats/php-mf2/issues/249
+	 */
+	public function testInvalidRootsIgnored() {
+		$input = <<<EOF
+<div class=" h-5 pt-2">
+	<div class="p-4 h-entry">
+		<a class="u-url" href="https://example.com/bar">bar </a>
+	</div>
+	<div class="p-4 h-entry">
+		<a class="u-url" href="https://example.com/foo">foo</a>
+	</div>
+</div>
+EOF;
+		$output = Mf2\parse($input);
+
+		# `items` should have length=2
+		$this->assertEquals(2, count($output['items']));
+
+		# each should be an h-entry
+		$this->assertEquals('h-entry', $output['items'][0]['type'][0]);
+		$this->assertEquals('h-entry', $output['items'][1]['type'][0]);
+	}
+
+	/**
+	 * @see https://github.com/microformats/php-mf2/issues/249
+	 */
+	public function testInvalidNestedRootsIgnored() {
+		$input = <<<EOF
+<div class=" h-5 pt-2">
+	<div class="p-4 h-entry">
+		<a class="u-url" href="https://example.com/bar">bar </a>
+	</div>
+	<div class="p-4 h-entry">
+		<a class="u-url" href="https://example.com/foo">foo</a>
+	</div>
+	<div class="p-4 h-5">
+		<a class="u-url" href="https://example.com/baz">baz</a>
+	</div>
+</div>
+EOF;
+		$output = Mf2\parse($input);
+
+		# `items` should have length=2
+		$this->assertEquals(2, count($output['items']));
+
+		# each should be an h-entry
+		$this->assertEquals('h-entry', $output['items'][0]['type'][0]);
+		$this->assertEquals('h-entry', $output['items'][1]['type'][0]);
+	}
+
+	/**
+	 * @see https://github.com/microformats/php-mf2/issues/249
+	 */
+	public function testInvalidNestedPropertiesIgnored1() {
+		$input = <<<EOF
+<div class=" h-feed pt-2">
+	<div class="p-4 h-entry">
+		<a class="u-url" href="https://example.com/bar">bar </a>
+	</div>
+	<div class="p-4 h-entry">
+		<a class="u-url" href="https://example.com/foo">foo</a>
+	</div>
+</div>
+EOF;
+		$output = Mf2\parse($input);
+
+		# `properties` should be empty
+		$this->assertEquals(0, count($output['items'][0]['properties']));
+
+		# `children` should have length=2
+		$this->assertArrayHasKey('children', $output['items'][0]);
+		$this->assertEquals(2, count($output['items'][0]['children']));
+
+		# each child should be an h-entry
+		$this->assertEquals('h-entry', $output['items'][0]['children'][0]['type'][0]);
+		$this->assertEquals('h-entry', $output['items'][0]['children'][1]['type'][0]);
+	}
+
+	/**
+	 * @see https://github.com/microformats/php-mf2/issues/246
+	 */
+	public function testInvalidNestedPropertiesIgnored2() {
+		$input = <<<EOF
+<article class="h-card">
+	 <h1> TITLE </h1>
+	 <img class="h-36 photo u-logo"
+					alt="An example alt title"
+					src="https://example.com/img.png"
+		/>
+		<p class="h-21 u-nickname"> John Doe </p>
+</article>
+EOF;
+		$output = Mf2\parse($input);
+
+		# `properties` should have length=3
+		$this->assertEquals(3, count($output['items'][0]['properties']));
+
+		# should have these properties
+		$this->assertArrayHasKey('logo', $output['items'][0]['properties']);
+		$this->assertArrayHasKey('nickname', $output['items'][0]['properties']);
+		$this->assertArrayHasKey('name', $output['items'][0]['properties']);
+	}
 }