From b7b312e3398fdd0d31e4784d3d2d75d029306f68 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Wed, 25 Dec 2024 14:59:54 +0100 Subject: [PATCH] Adding UriStringProvider interface --- composer.json | 1 + docs/uri/7.0/index.md | 3 + docs/uri/7.0/rfc3986.md | 18 ++++- interfaces/Contracts/UriInterface.php | 5 +- interfaces/Contracts/UriStringProvider.php | 71 +++++++++++++++++ uri/CHANGELOG.md | 2 + uri/Uri.php | 47 ++++++++++- uri/UriTest.php | 93 +++++++++++++++++++++- uri/composer.json | 1 + 9 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 interfaces/Contracts/UriStringProvider.php diff --git a/composer.json b/composer.json index 58543375..664d993b 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "require": { "php": "^8.1", "ext-bcmath": "*", + "ext-dom": "*", "ext-fileinfo": "*", "ext-gmp": "*", "ext-intl": "*", diff --git a/docs/uri/7.0/index.md b/docs/uri/7.0/index.md index 60cd0270..c3d53a5b 100644 --- a/docs/uri/7.0/index.md +++ b/docs/uri/7.0/index.md @@ -46,6 +46,9 @@ as an IPv4 address. In order to create Data URI from the content of a file you are required to also install the `fileinfo` extension otherwise an exception will be thrown. +To use the `toAnchor` method you need to have the `ext-dpm` extension +installed in your system. + Installation -------- diff --git a/docs/uri/7.0/rfc3986.md b/docs/uri/7.0/rfc3986.md index b7af78f7..1064b1bd 100644 --- a/docs/uri/7.0/rfc3986.md +++ b/docs/uri/7.0/rfc3986.md @@ -149,7 +149,7 @@ echo $uri->toNormalizedString(); //displays 'example://a/b/c/%7Bfoo%7D?foo%5B%5D echo $uri->toDisplayString(); //displays 'example://a/b/c/{foo}?foo[]=bar' ```` -File specific representation are also added to allow representing Unix and Windows Path. +File specific representation are added to allow representing Unix and Windows Path. ```php use League\Uri\Uri; @@ -164,6 +164,22 @@ $uri = Uri::new('file://localhost/etc/fstab'); echo $uri->toRfc8089(); //display 'file:/etc/fstab' ``` +HTML specific representation are added to allow adding URI to your HTML/Markdown page. + +```php +use League\Uri\Uri; + +$uri = Uri::new('eXAMPLE://a/./b/../b/%63/%7bfoo%7d?foo[]=bar'); +echo $uri->toMarkdown(); +//display '[example://a/b/c/{foo}?foo[]=bar](example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar) +echo $uri->toMarkdown('my link'); +//display '[my link](example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar) +echo $uri->toAnchorTag(); +// display 'example://a/b/c/{foo}?foo[]=bar' +echo $uri->toAnchorTag('my link'); +// display 'my link' +``` + ## Accessing URI properties Let's examine the result of building a URI: diff --git a/interfaces/Contracts/UriInterface.php b/interfaces/Contracts/UriInterface.php index 0cfafe72..4d60fea8 100644 --- a/interfaces/Contracts/UriInterface.php +++ b/interfaces/Contracts/UriInterface.php @@ -24,15 +24,12 @@ * * @method string|null getUsername() returns the user component of the URI. * @method string|null getPassword() returns the scheme-specific information about how to gain authorization to access the resource. - * @method string|null toUnixPath() returns the Unix filesystem path. The method returns null for any other scheme - * @method string|null toWindowsPath() returns the Windows filesystem path. The method returns null for any other scheme - * @method string|null toRfc8089() returns a string representation of a File URI according to RFC8089. The method returns null for any other scheme + * @method string|null getOrigin() returns the URI origin as described in the WHATWG URL Living standard specification * @method string toNormalizedString() returns the normalized string representation of the URI * @method array toComponents() returns an associative array containing all the URI components. * @method self normalize() returns a new URI instance with normalized components * @method self resolve(UriInterface $uri) resolves a URI against a base URI using RFC3986 rules * @method self relativize(UriInterface $uri) relativize a URI against a base URI using RFC3986 rules - * @method self|null getOrigin() returns the URI origin as described in the WHATWG URL Living standard specification * @method bool isOpaque() tells whether the given URI object represents an opaque URI. * @method bool isAbsolute() tells whether the URI represents an absolute URI. * @method bool isNetworkPath() tells whether the URI represents a network path URI. diff --git a/interfaces/Contracts/UriStringProvider.php b/interfaces/Contracts/UriStringProvider.php new file mode 100644 index 00000000..1ded31fd --- /dev/null +++ b/interfaces/Contracts/UriStringProvider.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\Contracts; + +use DOMException; +use JsonSerializable; + +interface UriStringProvider extends JsonSerializable +{ + /** + * Returns the string representation as a URI reference. + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + */ + public function toString(): string; + + /** + * Returns the normalized string representation of the URI. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-6.2 + */ + public function toNormalizedString(): ?string; + + /** + * Returns the human-readable string representation of the URI. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-6.2 + */ + public function toDisplayString(): ?string; + + /** + * Returns the string representation as a URI reference. + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @see ::__toString + */ + public function jsonSerialize(): string; + + /** + * Returns the HTML string representation of the anchor tag with the current instance as its href attribute. + * + * @throws DOMException + */ + public function toAnchorTag(?string $content = null, ?string $class = null, ?string $target = null): string; + + /** + * Returns the markdown string representation of the anchor tag with the current instance as its href attribute. + */ + public function toMarkdown(?string $content = null): string; + + /** + * Returns the Unix filesystem path. The method returns null for any other scheme. + */ + public function toUnixPath(): ?string; + + /** + * Returns the Windows filesystem path. The method returns null for any other scheme. + */ + public function toWindowsPath(): ?string; +} diff --git a/uri/CHANGELOG.md b/uri/CHANGELOG.md index 522155c1..a39fab74 100644 --- a/uri/CHANGELOG.md +++ b/uri/CHANGELOG.md @@ -27,6 +27,8 @@ All Notable changes to `League\Uri` will be documented in this file - `Uri::toUnixPath` returns the URI path as a Unix Path or `null` - `Uri::toWindowsPath` returns the URI path as a Windows Path or `null` - `Uri::toRfc8089` return the URI in a RFC8089 formator `null` +- `Uri::toAnchor` returns the HTML anchor string using the instance as the href attribute value +- `Uri::toMarkdown` returns the markdown link construct using the instance as the href attribute value ### Fixed diff --git a/uri/Uri.php b/uri/Uri.php index ee437ceb..32398b8b 100644 --- a/uri/Uri.php +++ b/uri/Uri.php @@ -14,11 +14,14 @@ namespace League\Uri; use Deprecated; +use DOMDocument; +use DOMException; use finfo; use League\Uri\Contracts\Conditionable; use League\Uri\Contracts\UriComponentInterface; use League\Uri\Contracts\UriException; use League\Uri\Contracts\UriInterface; +use League\Uri\Contracts\UriStringProvider; use League\Uri\Exceptions\ConversionFailed; use League\Uri\Exceptions\MissingFeature; use League\Uri\Exceptions\SyntaxError; @@ -75,7 +78,7 @@ * @phpstan-import-type ComponentMap from UriString * @phpstan-import-type InputComponentMap from UriString */ -final class Uri implements UriInterface, Conditionable +final class Uri implements UriInterface, Conditionable, UriStringProvider { /** * RFC3986 invalid characters. @@ -1039,6 +1042,42 @@ public function toDisplayString(): string return UriString::build($components); } + /** + * Returns the HTML string representation of the anchor tag with the current instance as its href attribute. + * + * @throws DOMException + */ + public function toAnchorTag(?string $content = null, ?string $class = null, ?string $target = null): string + { + $doc = new DOMDocument('1.0', 'utf-8'); + $doc->preserveWhiteSpace = false; + $doc->formatOutput = true; + $anchor = $doc->createElement('a'); + $anchor->setAttribute('href', $this->toString()); + if (null !== $class) { + $anchor->setAttribute('class', $class); + } + if (null !== $target) { + $anchor->setAttribute('target', $target); + } + + $anchor->appendChild($doc->createTextNode($content ?? $this->toDisplayString())); + $anchor = $doc->saveHTML($anchor); + if (false === $anchor) { + throw new DOMException('The link generation failed.'); + } + + return $anchor; + } + + /** + * Returns the markdown string representation of the anchor tag with the current instance as its href attribute. + */ + public function toMarkdown(?string $content = null): string + { + return '['.($content ?? $this->toDisplayString()).']('.$this->toString().')'; + } + /** * Returns the Unix filesystem path. * @@ -1208,9 +1247,9 @@ public function getFragment(): ?string return $this->fragment; } - public function getOrigin(): ?self + public function getOrigin(): ?string { - return null === $this->origin ? null : Uri::new($this->origin); + return $this->origin; } public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static @@ -1429,7 +1468,7 @@ public function isCrossOrigin(UriInterface|Stringable|string $uri): bool return true; } - return $this->origin !== (string) $origin; + return $this->origin !== $origin; } public function isSameOrigin(Stringable|string $uri): bool diff --git a/uri/UriTest.php b/uri/UriTest.php index 44564b62..587eca4d 100644 --- a/uri/UriTest.php +++ b/uri/UriTest.php @@ -717,7 +717,7 @@ public static function sameValueAsProvider(): array #[DataProvider('getOriginProvider')] public function testGetOrigin(Psr7UriInterface|Uri|string $uri, ?string $expectedOrigin): void { - self::assertSame($expectedOrigin, Uri::new($uri)->getOrigin()?->toString()); + self::assertSame($expectedOrigin, Uri::new($uri)->getOrigin()); } public static function getOriginProvider(): array @@ -981,4 +981,95 @@ public static function providesUriToDisplay(): iterable 'output' => 'http://bébé.be', ]; } + + #[Test] + #[DataProvider('providesUriToMarkdown')] + public function it_will_generate_the_markdown_code_for_the_instance(string $uri, ?string $content, string $expected): void + { + self::assertSame($expected, Uri::new($uri)->toMarkdown($content)); + } + + public static function providesUriToMarkdown(): iterable + { + yield 'empty string' => [ + 'uri' => '', + 'content' => '', + 'expected' => '[]()', + ]; + + yield 'URI with a specific content' => [ + 'uri' => 'http://example.com/foo/bar', + 'content' => 'this is a link', + 'expected' => '[this is a link](http://example.com/foo/bar)', + ]; + + yield 'URI without content' => [ + 'uri' => 'http://Bébé.be', + 'content' => null, + 'expected' => '[http://bébé.be](http://xn--bb-bjab.be)', + ]; + } + + #[Test] + #[DataProvider('providesUriToHTML')] + public function it_will_generate_the_html_code_for_the_instance( + string $uri, + ?string $content, + ?string $class, + ?string $target, + string $expected + ): void { + self::assertSame($expected, Uri::new($uri)->toAnchorTag($content, $class, $target)); + } + + public static function providesUriToHTML(): iterable + { + yield 'empty string' => [ + 'uri' => '', + 'content' => '', + 'class' => null, + 'target' => null, + 'expected' => '', + ]; + + yield 'URI with a specific content' => [ + 'uri' => 'http://example.com/foo/bar', + 'content' => 'this is a link', + 'class' => null, + 'target' => null, + 'expected' => 'this is a link', + ]; + + yield 'URI without content' => [ + 'uri' => 'http://Bébé.be', + 'content' => null, + 'class' => null, + 'target' => null, + 'expected' => 'http://bébé.be', + ]; + + yield 'URI without content and with class' => [ + 'uri' => 'http://Bébé.be', + 'content' => null, + 'class' => 'foo bar', + 'target' => null, + 'expected' => 'http://bébé.be', + ]; + + yield 'URI without content and with target' => [ + 'uri' => 'http://Bébé.be', + 'content' => null, + 'class' => null, + 'target' => '_blank', + 'expected' => 'http://bébé.be', + ]; + + yield 'URI without content, with target and class' => [ + 'uri' => 'http://Bébé.be', + 'content' => null, + 'class' => 'foo bar', + 'target' => '_blank', + 'expected' => 'http://bébé.be', + ]; + } } diff --git a/uri/composer.json b/uri/composer.json index e1c4be3e..d88b6343 100644 --- a/uri/composer.json +++ b/uri/composer.json @@ -56,6 +56,7 @@ "league/uri-schemes": "^1.0" }, "suggest": { + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-bcmath": "to improve IPV4 host parsing", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing",