diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 189105d..2f3eecb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,5 +12,3 @@ jobs: uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main cs: uses: innmind/github-workflows/.github/workflows/cs.yml@main - with: - php-version: '8.2' diff --git a/.github/workflows/extensive.yml b/.github/workflows/extensive.yml new file mode 100644 index 0000000..257f139 --- /dev/null +++ b/.github/workflows/extensive.yml @@ -0,0 +1,12 @@ +name: Extensive CI + +on: + push: + tags: + - '*' + paths: + - '.github/workflows/extensive.yml' + +jobs: + blackbox: + uses: innmind/github-workflows/.github/workflows/extensive.yml@main diff --git a/CHANGELOG.md b/CHANGELOG.md index 25184c0..2306877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [Unreleased] + +### Changed + +- Requires PHP `8.4` +- Requires `innmind/xml:~9.0` + ## 7.0.0 - 2025-06-07 ### Added diff --git a/blackbox.php b/blackbox.php index 50022e2..67a2468 100644 --- a/blackbox.php +++ b/blackbox.php @@ -10,6 +10,10 @@ }; Application::new($argv) + ->when( + \getenv('BLACKBOX_SET_SIZE') !== false, + static fn(Application $app) => $app->scenariiPerProof((int) \getenv('BLACKBOX_SET_SIZE')), + ) ->when( \getenv('ENABLE_COVERAGE') !== false, static fn(Application $app) => $app diff --git a/composer.json b/composer.json index 548b2b1..eb4a8d8 100644 --- a/composer.json +++ b/composer.json @@ -15,13 +15,12 @@ "issues": "http://github.com/Innmind/Html/issues" }, "require": { - "php": "~8.2", - "innmind/xml": "~8.0", - "innmind/filesystem": "~8.1", - "innmind/immutable": "~5.16", - "innmind/validation": "~2.0", - "symfony/dom-crawler": ">=6.3 <7.0.7", - "innmind/url": "~4.0" + "php": "~8.4", + "innmind/xml": "~9.0", + "innmind/filesystem": "~9.0", + "innmind/immutable": "~6.0", + "innmind/validation": "~3.0", + "innmind/url": "~5.0" }, "autoload": { "psr-4": { @@ -35,7 +34,7 @@ }, "require-dev": { "innmind/black-box": "^6.4.1", - "innmind/static-analysis": "^1.2.1", + "innmind/static-analysis": "~1.3", "innmind/coding-standard": "~2.0" } } diff --git a/src/Reader.php b/src/Reader.php index 6ea4630..35a2b5e 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -10,7 +10,6 @@ }; use Innmind\Filesystem\File\Content; use Innmind\Immutable\Attempt; -use Symfony\Component\DomCrawler\Crawler; /** * @psalm-immutable @@ -26,20 +25,20 @@ private function __construct(private Translator $translate) */ public function __invoke(Content $html): Attempt { - /** @psalm-suppress ImpureMethodCall */ - $firstNode = (new Crawler($html->toString(), useHtml5Parser: false))->getNode(0); + $content = $html->toString(); - if (!$firstNode instanceof \DOMNode) { - /** @var Attempt */ - return Attempt::error(new \RuntimeException('Failed to parse html content')); + if ($content === '') { + return Attempt::error(new \RuntimeException('Empty content')); } - /** @psalm-suppress RedundantCondition */ - while ($firstNode->parentNode instanceof \DOMNode) { - $firstNode = $firstNode->parentNode; + try { + return ($this->translate)(\Dom\HTMLDocument::createFromString( + $content, + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + )); + } catch (\Throwable $e) { + return Attempt::error($e); } - - return ($this->translate)($firstNode); } public static function new(): self diff --git a/src/Translator.php b/src/Translator.php index 3bbb353..425a624 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -37,11 +37,9 @@ private function __construct( } /** - * @psalm-suppress UndefinedClass Since the package still supports PHP 8.2 - * * @return Attempt */ - public function __invoke(\DOMNode|\Dom\Node $node): Attempt + public function __invoke(\Dom\Node $node): Attempt { return $this ->buildDocument($node) @@ -59,11 +57,9 @@ public static function new(): self } /** - * @psalm-suppress UndefinedClass Since the package still supports PHP 8.2 - * * @return Attempt */ - private function child(\DOMNode|\Dom\Node $node): Attempt + private function child(\Dom\Node $node): Attempt { /** @var Attempt Psalm doesn't understand the filter */ return ($this->translate)($node) @@ -74,39 +70,27 @@ private function child(\DOMNode|\Dom\Node $node): Attempt ->or(Instance::of(Node::class)), static fn() => new \RuntimeException('Invalid document node'), ) - ->match( - Attempt::result(...), - Attempt::error(...), - ); + ->attempt(static fn($e) => $e); } /** - * @psalm-suppress UndefinedClass Since the package still supports PHP 8.2 * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall * @psalm-suppress UndefinedPropertyFetch * * @return Attempt */ - private function buildDocument(\DOMNode|\Dom\Node $node): Attempt + private function buildDocument(\Dom\Node $node): Attempt { /** @var Sequence */ $children = Sequence::of(); return Maybe::just($node) - ->keep( - Instance::of(\DOMDocument::class)->or( - Instance::of(\Dom\Document::class), - ), - ) + ->keep(Instance::of(\Dom\Document::class)) ->attempt(static fn() => new \RuntimeException('Not a document')) ->flatMap( fn($document) => Sequence::of(...\array_values(\iterator_to_array($document->childNodes))) - ->keep( - Instance::of(\DOMNode::class)->or( - Instance::of(\Dom\Node::class), - ), - ) + ->keep(Instance::of(\Dom\Node::class)) ->exclude(static fn($child) => $child->nodeType === \XML_DOCUMENT_TYPE_NODE) ->sink($children) ->attempt( @@ -129,11 +113,10 @@ private function buildDocument(\DOMNode|\Dom\Node $node): Attempt /** * @psalm-pure * @psalm-suppress ImpurePropertyFetch - * @psalm-suppress UndefinedClass Since the package still supports PHP 8.2 * * @return Maybe */ - private static function buildDoctype(\DOMDocumentType|\Dom\DocumentType $type): Maybe + private static function buildDoctype(\Dom\DocumentType $type): Maybe { /** @psalm-suppress MixedArgument */ return Type::maybe( diff --git a/tests/DocumentTest.php b/tests/DocumentTest.php index 6506631..8005466 100644 --- a/tests/DocumentTest.php +++ b/tests/DocumentTest.php @@ -85,8 +85,8 @@ public function testPrependChild() $this->assertInstanceOf(Document::class, $document2); $this->assertSame($document->type(), $document2->type()); $this->assertNotSame($document->children(), $document2->children()); - $this->assertCount(3, $document->children()); - $this->assertCount(4, $document2->children()); + $this->assertSame(3, $document->children()->size()); + $this->assertSame(4, $document2->children()->size()); $this->assertSame( $node, $document2->children()->get(0)->match( @@ -127,8 +127,8 @@ public function testAppendChild() $this->assertInstanceOf(Document::class, $document2); $this->assertSame($document->type(), $document2->type()); $this->assertNotSame($document->children(), $document2->children()); - $this->assertCount(3, $document->children()); - $this->assertCount(4, $document2->children()); + $this->assertSame(3, $document->children()->size()); + $this->assertSame(4, $document2->children()->size()); $this->assertEquals( $document->children()->get(0), $document2->children()->get(0), diff --git a/tests/Element/ScriptTest.php b/tests/Element/ScriptTest.php index 4355e29..563439d 100644 --- a/tests/Element/ScriptTest.php +++ b/tests/Element/ScriptTest.php @@ -26,7 +26,7 @@ public function testInterface() ''."\n", $script->asContent()->toString(), ); - $this->assertCount(1, $script->children()); + $this->assertSame(1, $script->children()->size()); } public function testWithAttributes() diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index d3da86b..9d4e1cc 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -37,14 +37,17 @@ public function testReadSimple() static fn($node) => $node, static fn() => null, ); + + // For some reason the \Dom\HTMLDocument API doesn't respect the + // indentation of the input document. + $space = ' '; $expected = << - - + foo - - + $space + HTML; $this->assertInstanceOf(Document::class, $node); diff --git a/tests/Translator/NodeTranslator/ATranslatorTest.php b/tests/Translator/NodeTranslator/ATranslatorTest.php index 181d10f..d1f8f71 100644 --- a/tests/Translator/NodeTranslator/ATranslatorTest.php +++ b/tests/Translator/NodeTranslator/ATranslatorTest.php @@ -14,11 +14,13 @@ class ATranslatorTest extends TestCase { public function testTranslate() { - $dom = new \DOMDocument; - $dom->loadHTML('foo'); + $dom = \Dom\HTMLDocument::createFromString( + 'foo', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $a = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), )->match( static fn($a) => $a, static fn() => null, @@ -27,21 +29,23 @@ public function testTranslate() $this->assertInstanceOf(A::class, $a); $this->assertSame('/', $a->href()->toString()); $a = $a->normalize(); - $this->assertCount(2, $a->attributes()); + $this->assertSame(2, $a->attributes()->size()); $this->assertSame('whatever', $a->attribute('class')->match( static fn($attribute) => $attribute->value(), static fn() => null, )); - $this->assertCount(1, $a->children()); + $this->assertSame(1, $a->children()->size()); } public function testReturnNothingWhenMissingHrefAttribute() { - $dom = new \DOMDocument; - $dom->loadHTML('foo'); + $dom = \Dom\HTMLDocument::createFromString( + 'foo', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $result = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), ) ->maybe() ->keep(Instance::of(A::class)); diff --git a/tests/Translator/NodeTranslator/BaseTranslatorTest.php b/tests/Translator/NodeTranslator/BaseTranslatorTest.php index 588bf74..cd776b6 100644 --- a/tests/Translator/NodeTranslator/BaseTranslatorTest.php +++ b/tests/Translator/NodeTranslator/BaseTranslatorTest.php @@ -14,11 +14,13 @@ class BaseTranslatorTest extends TestCase { public function testTranslate() { - $dom = new \DOMDocument; - $dom->loadHTML(''); + $dom = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $base = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), )->match( static fn($base) => $base, static fn() => null, @@ -27,7 +29,7 @@ public function testTranslate() $this->assertInstanceOf(Base::class, $base); $this->assertSame('/', $base->href()->toString()); $base = $base->normalize(); - $this->assertCount(2, $base->attributes()); + $this->assertSame(2, $base->attributes()->size()); $this->assertSame('_blank', $base->attribute('target')->match( static fn($attribute) => $attribute->value(), static fn() => null, @@ -36,11 +38,13 @@ public function testTranslate() public function testReturnNothingWhenMissingHrefAttribute() { - $dom = new \DOMDocument; - $dom->loadHTML(''); + $dom = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $result = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), ) ->maybe() ->keep(Instance::of(Base::class)); diff --git a/tests/Translator/NodeTranslator/DocumentTranslatorTest.php b/tests/Translator/NodeTranslator/DocumentTranslatorTest.php index e0bc0e8..21c53d8 100644 --- a/tests/Translator/NodeTranslator/DocumentTranslatorTest.php +++ b/tests/Translator/NodeTranslator/DocumentTranslatorTest.php @@ -13,8 +13,7 @@ class DocumentTranslatorTest extends TestCase { public function testTranslate() { - $document = new \DOMDocument; - $document->loadHtml(''); + $document = \Dom\HTMLDocument::createFromString(''); $node = Translator::new()( $document, @@ -25,11 +24,12 @@ public function testTranslate() $this->assertInstanceOf(Document::class, $node); $this->assertSame('html', $node->type()->name()); - $this->assertCount(1, $node->children()); + $this->assertSame(1, $node->children()->size()); $this->assertSame( << + @@ -40,8 +40,10 @@ public function testTranslate() public function testTranslateWithoutDoctype() { - $document = new \DOMDocument; - $document->loadHtml(''); + $document = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_NOERROR, + ); $node = Translator::new()( $document, @@ -51,15 +53,17 @@ public function testTranslateWithoutDoctype() ); $this->assertSame( - '', + '', $node->type()->toString(), ); } public function testTranslateWithoutChildren() { - $document = new \DOMDocument; - $document->loadHtml(''); + $document = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $node = Translator::new()( $document, diff --git a/tests/Translator/NodeTranslator/ImgTranslatorTest.php b/tests/Translator/NodeTranslator/ImgTranslatorTest.php index f1f6972..5587f08 100644 --- a/tests/Translator/NodeTranslator/ImgTranslatorTest.php +++ b/tests/Translator/NodeTranslator/ImgTranslatorTest.php @@ -14,11 +14,13 @@ class ImgTranslatorTest extends TestCase { public function testTranslate() { - $dom = new \DOMDocument; - $dom->loadHTML('bar'); + $dom = \Dom\HTMLDocument::createFromString( + 'bar', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $img = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), )->match( static fn($img) => $img, static fn() => null, @@ -27,7 +29,7 @@ public function testTranslate() $this->assertInstanceOf(Img::class, $img); $this->assertSame('foo.png', $img->src()->toString()); $img = $img->normalize(); - $this->assertCount(2, $img->attributes()); + $this->assertSame(2, $img->attributes()->size()); $this->assertSame('bar', $img->attribute('alt')->match( static fn($attribute) => $attribute->value(), static fn() => null, @@ -36,11 +38,13 @@ public function testTranslate() public function testReturnNothingWhenMissingHrefAttribute() { - $dom = new \DOMDocument; - $dom->loadHTML(''); + $dom = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $result = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), ) ->maybe() ->keep(Instance::of(Img::class)); diff --git a/tests/Translator/NodeTranslator/LinkTranslatorTest.php b/tests/Translator/NodeTranslator/LinkTranslatorTest.php index c7038c6..76d540b 100644 --- a/tests/Translator/NodeTranslator/LinkTranslatorTest.php +++ b/tests/Translator/NodeTranslator/LinkTranslatorTest.php @@ -14,11 +14,13 @@ class LinkTranslatorTest extends TestCase { public function testTranslate() { - $dom = new \DOMDocument; - $dom->loadHTML(''); + $dom = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $link = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), )->match( static fn($link) => $link, static fn() => null, @@ -28,7 +30,7 @@ public function testTranslate() $this->assertSame('/', $link->href()->toString()); $this->assertSame('next', $link->relationship()); $link = $link->normalize(); - $this->assertCount(3, $link->attributes()); + $this->assertSame(3, $link->attributes()->size()); $this->assertSame('fr', $link->attribute('hreflang')->match( static fn($attribute) => $attribute->value(), static fn() => null, @@ -37,11 +39,13 @@ public function testTranslate() public function testTranslateWithoutRelationship() { - $dom = new \DOMDocument; - $dom->loadHTML(''); + $dom = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $link = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), )->match( static fn($link) => $link, static fn() => null, @@ -51,7 +55,7 @@ public function testTranslateWithoutRelationship() $this->assertSame('/', $link->href()->toString()); $this->assertSame('related', $link->relationship()); $link = $link->normalize(); - $this->assertCount(3, $link->attributes()); + $this->assertSame(3, $link->attributes()->size()); $this->assertSame('fr', $link->attribute('hreflang')->match( static fn($attribute) => $attribute->value(), static fn() => null, @@ -64,11 +68,13 @@ public function testTranslateWithoutRelationship() public function testReturnNothingWhenMissingHrefAttribute() { - $dom = new \DOMDocument; - $dom->loadHTML(''); + $dom = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $result = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), ) ->maybe() ->keep(Instance::of(Link::class)); diff --git a/tests/Translator/NodeTranslator/ScriptTranslatorTest.php b/tests/Translator/NodeTranslator/ScriptTranslatorTest.php index 7440e9d..3d0abec 100644 --- a/tests/Translator/NodeTranslator/ScriptTranslatorTest.php +++ b/tests/Translator/NodeTranslator/ScriptTranslatorTest.php @@ -13,11 +13,13 @@ class ScriptTranslatorTest extends TestCase { public function testTranslate() { - $dom = new \DOMDocument; - $dom->loadHTML(''); + $dom = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $script = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), )->match( static fn($script) => $script, static fn() => null, @@ -29,7 +31,7 @@ public function testTranslate() ''."\n", $script->asContent()->toString(), ); - $this->assertCount(1, $script->attributes()); + $this->assertSame(1, $script->attributes()->size()); $this->assertSame( 'text/javascript', $script->attribute('type')->match( @@ -37,16 +39,18 @@ public function testTranslate() static fn() => null, ), ); - $this->assertCount(1, $script->children()); + $this->assertSame(1, $script->children()->size()); } public function testTranslateWithoutCode() { - $dom = new \DOMDocument; - $dom->loadHTML(''); + $dom = \Dom\HTMLDocument::createFromString( + '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_NOERROR, + ); $script = Translator::new()( - $dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0), + $dom->childNodes->item(0), )->match( static fn($script) => $script, static fn() => null, @@ -58,7 +62,7 @@ public function testTranslateWithoutCode() ''."\n", $script->asContent()->toString(), ); - $this->assertCount(0, $script->attributes()); - $this->assertCount(1, $script->children()); + $this->assertSame(0, $script->attributes()->size()); + $this->assertSame(1, $script->children()->size()); } } diff --git a/tests/Visitor/ElementsTest.php b/tests/Visitor/ElementsTest.php index bca1af4..38686b9 100644 --- a/tests/Visitor/ElementsTest.php +++ b/tests/Visitor/ElementsTest.php @@ -36,7 +36,7 @@ public function testExtractElement() $h1s = Elements::of('h1')($node); $this->assertInstanceOf(Sequence::class, $h1s); - $this->assertCount(26, $h1s); + $this->assertSame(26, $h1s->size()); } public function testEmptySetWhenNoElementFound() @@ -44,6 +44,6 @@ public function testEmptySetWhenNoElementFound() $elements = Elements::of('foo')(Element::of(Name::of('whatever'))); $this->assertInstanceOf(Sequence::class, $elements); - $this->assertCount(0, $elements); + $this->assertSame(0, $elements->size()); } }