Skip to content

Commit 4a4c4c3

Browse files
committed
Move extra string representation fron Uri to Modifier class
1 parent edba17a commit 4a4c4c3

File tree

14 files changed

+160
-371
lines changed

14 files changed

+160
-371
lines changed

components/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ All Notable changes to `League\Uri\Components` will be documented in this file
1818
- `Modifier::wrap` static method which wraps the underlying URI object used by the Modifier class.
1919
- `Modifier::unwrap` method which returns the underlying URI object used by the Modifier class.
2020
- `Modifier::prefixQueryPairs` and `Modifier::prefixQueryParameters` to prefix Query using the pair key or the parameter names
21+
- `Modifier::toMarkdownAnchor` returns the Markdown string representation of the anchor tag with the current instance as its href attribute.
22+
- `Modifier::toHtmlAnchor` returns the HTML string representation of the anchor tag with the current instance as its href attribute.
2123
- `tryNew` named constructor added to all classes to return a new instance on success or `null` on failure.
2224
- `Query::decoded` the string representation of the component decoded.
2325
- `Query::normalized`

components/Modifier.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ public function toDisplayString(): string
115115
/**
116116
* Returns the Markdown string representation of the anchor tag with the current instance as its href attribute.
117117
*/
118-
public function toMarkdownAnchor(?string $linkTextTemplate = null): string
118+
public function toMarkdownAnchor(?string $textContent = null): string
119119
{
120-
return '['.strtr($linkTextTemplate ?? '{uri}', ['{uri}' => $this->toDisplayString()]).']('.$this->toString().')';
120+
return '['.strtr($textContent ?? '{uri}', ['{uri}' => $this->toDisplayString()]).']('.$this->toString().')';
121121
}
122122

123123
/**
@@ -127,14 +127,18 @@ public function toMarkdownAnchor(?string $linkTextTemplate = null): string
127127
*
128128
* @throws DOMException
129129
*/
130-
public function toHtmlAnchor(?string $linkTextTemplate = null, iterable $attributes = []): string
130+
public function toHtmlAnchor(Stringable|string|null $textContent = null, iterable $attributes = []): string
131131
{
132132
FeatureDetection::supportsDom();
133-
133+
$uriString = $this->toString();
134+
$rfc3987String = UriString::toIriString($uriString);
134135
$doc = class_exists(HTMLDocument::class) ? HTMLDocument::createEmpty() : new DOMDocument(encoding:'utf-8');
135136
$element = $doc->createElement('a');
136-
$element->setAttribute('href', $this->toString());
137-
$element->appendChild($doc->createTextNode(strtr($linkTextTemplate ?? '{uri}', ['{uri}' => $this->toDisplayString()])));
137+
$element->setAttribute('href', $uriString);
138+
$element->appendChild(match (true) {
139+
null === $textContent => $doc->createTextNode($rfc3987String),
140+
default => $doc->createTextNode(strtr((string) $textContent, ['{uri}' => $rfc3987String])),
141+
});
138142

139143
foreach ($attributes as $name => $value) {
140144
if ('href' === strtolower($name) || null === $value) {
@@ -154,8 +158,7 @@ public function toHtmlAnchor(?string $linkTextTemplate = null, iterable $attribu
154158
$element->setAttribute($name, $value);
155159
}
156160

157-
$html = $doc->saveHTML($element);
158-
false !== $html || throw new DOMException('The HTML generation failed.');
161+
false !== ($html = $doc->saveHTML($element)) || throw new DOMException('The HTML generation failed.');
159162

160163
return $html;
161164
}

components/ModifierTest.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace League\Uri;
1313

14+
use DOMException;
1415
use GuzzleHttp\Psr7\Utils;
1516
use League\Uri\Components\DataPath;
1617
use League\Uri\Components\FragmentDirectives;
@@ -1115,4 +1116,101 @@ public static function resolveProvider(): array
11151116
'not same origin' => [self::BASE_URI, 'ftp://a/b/c/d', 'ftp://a/b/c/d'],
11161117
];
11171118
}
1119+
1120+
#[Test]
1121+
#[DataProvider('providesUriToMarkdown')]
1122+
public function it_will_generate_the_markdown_code_for_the_instance(string $uri, ?string $content, string $expected): void
1123+
{
1124+
self::assertSame($expected, Modifier::wrap($uri)->toMarkdownAnchor($content));
1125+
}
1126+
1127+
public static function providesUriToMarkdown(): iterable
1128+
{
1129+
yield 'empty string' => [
1130+
'uri' => '',
1131+
'content' => '',
1132+
'expected' => '[]()',
1133+
];
1134+
1135+
yield 'URI with a specific content' => [
1136+
'uri' => 'http://example.com/foo/bar',
1137+
'content' => 'this is a link',
1138+
'expected' => '[this is a link](http://example.com/foo/bar)',
1139+
];
1140+
1141+
yield 'URI without content' => [
1142+
'uri' => 'http://Bébé.be',
1143+
'content' => null,
1144+
'expected' => '[http://bébé.be](http://xn--bb-bjab.be)',
1145+
];
1146+
}
1147+
1148+
#[Test]
1149+
#[DataProvider('providesUriToAnchorTagHTML')]
1150+
public function it_will_generate_the_html_anchor_tag_code_for_the_instance(string $uri, ?string $content, array $parameters, string $expected): void
1151+
{
1152+
self::assertSame($expected, Modifier::wrap($uri)->toHtmlAnchor($content, $parameters));
1153+
}
1154+
1155+
public static function providesUriToAnchorTagHTML(): iterable
1156+
{
1157+
yield 'empty string' => [
1158+
'uri' => '',
1159+
'content' => '',
1160+
'parameters' => [],
1161+
'expected' => '<a href=""></a>',
1162+
];
1163+
1164+
yield 'URI with a specific content' => [
1165+
'uri' => 'http://example.com/foo/bar',
1166+
'content' => 'this is a link',
1167+
'parameters' => [],
1168+
'expected' => '<a href="http://example.com/foo/bar">this is a link</a>',
1169+
];
1170+
1171+
yield 'URI without content' => [
1172+
'uri' => 'http://Bébé.be',
1173+
'content' => null,
1174+
'parameters' => [],
1175+
'expected' => '<a href="http://xn--bb-bjab.be">http://bébé.be</a>',
1176+
];
1177+
1178+
yield 'URI without content and with class' => [
1179+
'uri' => 'http://Bébé.be',
1180+
'content' => null,
1181+
'parameters' => [
1182+
'class' => ['foo', 'bar'],
1183+
'target' => null,
1184+
],
1185+
'expected' => '<a href="http://xn--bb-bjab.be" class="foo bar">http://bébé.be</a>',
1186+
];
1187+
1188+
yield 'URI without content and with target' => [
1189+
'uri' => 'http://Bébé.be',
1190+
'content' => null,
1191+
'parameters' => [
1192+
'class' => null,
1193+
'target' => '_blank',
1194+
],
1195+
'expected' => '<a href="http://xn--bb-bjab.be" target="_blank">http://bébé.be</a>',
1196+
];
1197+
1198+
yield 'URI without content, with target and class' => [
1199+
'uri' => 'http://Bébé.be',
1200+
'content' => null,
1201+
'parameters' => [
1202+
'class' => 'foo bar',
1203+
'target' => '_blank',
1204+
],
1205+
'expected' => '<a href="http://xn--bb-bjab.be" class="foo bar" target="_blank">http://bébé.be</a>',
1206+
];
1207+
}
1208+
1209+
#[Test]
1210+
public function it_will_fail_to_generate_an_anchor_tag_html_for_the_instance(): void
1211+
{
1212+
$this->expectException(DOMException::class);
1213+
1214+
Modifier::wrap('https://example.com')->toHtmlAnchor(attributes: ["bébé\r\n" => 'yes']);
1215+
}
11181216
}

components/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"ext-gmp": "to improve IPV4 host parsing",
3939
"ext-intl": "to handle IDN host with the best performance",
4040
"jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain",
41+
"league/uri-polyfill" : "Needed to backport the PHP URI extension for older versions of PHP",
4142
"php-64bit": "to improve IPV4 host parsing",
4243
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present",
4344
"rowbot/url": "to handle WHATWG URL",

docs/uri/7.0/rfc3986.md

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,6 @@ echo $uri = //returns 'file:///etc/fstab'
105105

106106
<p class="message-info"><code>fromRfc8089</code> is added since version <code>7.4.0</code></p>
107107

108-
It is also possible to instantiate a new instance from the following HTTP related object or string>
109-
110-
~~~php
111-
$uri = Uri::fromMarkdownAnchor('[overview](https://uri.thephpleague.com/uri/7.0/)');
112-
echo $uri; //returns 'https://uri.thephpleague.com/uri/7.0/'
113-
114-
$uri = Uri::fromHtmlAnchor('<a href="/domain-parser/1.0/">uri-hostname-parser</a>');
115-
echo $uri; //returns '/domain-parser/1.0/'
116-
~~~
117-
118-
<p class="message-info">The named constructor are available since version <code>7.6.0</code></p>
119-
<p class="message-notice">To use the named constructor in relation to HTML tag, the <code>ext-dom</code> extension must be present.</p>
120-
121108
## URI string representation
122109

123110
The `Uri` class handles URI according to RFC3986 as such you can retrieve its string representation using the
@@ -133,7 +120,7 @@ echo $uri; //displays RFC3986 string representation
133120
But `Uri` can have multiple string representation depending on its scheme or context. As
134121
such the package provides several other string representations.
135122

136-
The `Uri` instance can be json encoded using the same URI representation from JavaScript to allow
123+
The `Uri` instance can be JSON encoded using the same URI representation from JavaScript to allow
137124
easier interoperability
138125

139126
```php
@@ -154,21 +141,7 @@ echo $uri->toString(); //displays 'example://a/./b/../b/%63/%7bfoo%7d?foo
154141
echo $uri->toDisplayString(); //displays 'example://a/./b/../b/c/{foo}?foo[]=bar'
155142
````
156143

157-
HTML specific representation are added to allow adding URI to your HTML/Markdown page.
158-
159-
```php
160-
$uri = Uri::new('eXAMPLE://a/./b/../b/%63/%7bfoo%7d?foo[]=bar');
161-
echo $uri->toMarkdownAnchor();
162-
//display '[example://a/b/c/{foo}?foo[]=bar](example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar)
163-
echo $uri->toMarkdownAnchor('my link');
164-
//display '[my link](example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar)
165-
echo $uri->toHtmlAnchor();
166-
// display '<a href="example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar">example://a/b/c/{foo}?foo[]=bar</a>'
167-
echo $uri->toHtmlAnchor('my link', ['class' => ['red', 'notice']]);
168-
// display '<a class="red notice" href="example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar">my link</a>'
169-
```
170-
171-
File specific representation are added to allow representing Unix and Windows Path.
144+
For `file` scheme, specifics representations are added to allow representing Unix and Windows Path.
172145

173146
```php
174147
$uri = Uri::new('file:///c:/windows/My%20Documents%20100%2520/foo.txt');

interfaces/UriString.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Stringable;
2121
use Throwable;
2222

23+
use function array_map;
2324
use function array_merge;
2425
use function array_pop;
2526
use function array_reduce;
@@ -33,6 +34,7 @@
3334
use function preg_match;
3435
use function rawurldecode;
3536
use function sprintf;
37+
use function str_replace;
3638
use function strpos;
3739
use function strtolower;
3840
use function substr;
@@ -194,6 +196,45 @@ final class UriString
194196
*/
195197
private const MAXIMUM_HOST_CACHED = 100;
196198

199+
/**
200+
* Generate an IRI string representation (RFC3987) from its parsed representation
201+
* returned by League\UriString::parse() or PHP's parse_url.
202+
*
203+
* If you supply your own array, you are responsible for providing
204+
* valid components without their URI delimiters.
205+
*
206+
* @link https://tools.ietf.org/html/rfc3986#section-5.3
207+
* @link https://tools.ietf.org/html/rfc3986#section-7.5
208+
*/
209+
public static function toIriString(Stringable|string $uri): string
210+
{
211+
$components = UriString::parse($uri);
212+
$port = null;
213+
if (isset($components['port'])) {
214+
$port = (int) $components['port'];
215+
unset($components['port']);
216+
}
217+
218+
if (null !== $components['host']) {
219+
$components['host'] = IdnaConverter::toUnicode($components['host'])->domain();
220+
}
221+
222+
$components['path'] = Encoder::decodePath($components['path']);
223+
$components['user'] = Encoder::decodeNecessary($components['user']);
224+
$components['pass'] = Encoder::decodeNecessary($components['pass']);
225+
$components['query'] = Encoder::decodeQuery($components['query']);
226+
$components['fragment'] = Encoder::decodeFragment($components['fragment']);
227+
228+
return self::build([
229+
...array_map(fn (?string $value) => match (true) {
230+
null === $value,
231+
!str_contains($value, '%20') => $value,
232+
default => str_replace('%20', ' ', $value),
233+
}, $components),
234+
...['port' => $port],
235+
]);
236+
}
237+
197238
/**
198239
* Generate a URI string representation from its parsed representation
199240
* returned by League\UriString::parse() or PHP's parse_url.

phpstan-dist.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ parameters:
88
- uri
99
- components
1010
- interfaces
11+
- polyfill
1112
ignoreErrors:
1213
- message: '#Cannot cast mixed to string.#'
1314
path: interfaces/IPv4/BCMathCalculator.php
@@ -18,6 +19,5 @@ parameters:
1819
- identifier: missingType.iterableValue
1920
- '#deprecated class League\\Uri\\BaseUri#'
2021
- '#deprecated interface League\\Uri\\Contracts\\UriAccess#'
21-
- '#\Dom\\HTMLDocument#'
2222
reportUnmatchedIgnoredErrors: true
2323
treatPhpDocTypesAsCertain: false

phpstan.neon

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,5 @@ parameters:
2020
- '#Attribute class Deprecated does not exist.#'
2121
- '#deprecated class League\\Uri\\BaseUri#'
2222
- '#deprecated interface League\\Uri\\Contracts\\UriAccess#'
23-
- '#\Dom\\HTMLDocument#'
2423
reportUnmatchedIgnoredErrors: true
2524
treatPhpDocTypesAsCertain: false

polyfill/lib/WhatWg/Url.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ private function setUnicodeHost(): ?string
223223
return $result->getDomain();
224224
}
225225

226-
private function setAsciiHost(): ?string
226+
private function setAsciiHost(): string
227227
{
228228
$host = $this->url->hostname;
229229
if ('' === $host || null === $host || 1 !== preg_match(self::REGEXP_IDNA_PATTERN, $host)) {

uri/CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ All Notable changes to `League\Uri` will be documented in this file
2323
- `Uri::isSameOrigin` tells whether two URI share the same origin.
2424
- `Uri::getOrigin` returns the URI origin as described in the WHATWG URL Living standard specification.
2525
- `Uri::toDisplayString` returns the human-readable string representation of the URI as an IRI.
26-
- `Uri::toMarkdownAnchor` returns the Markdown string representation of the anchor tag with the current instance as its href attribute.
27-
- `Uri::toHtmlAnchor` returns the HTML string representation of the anchor tag with the current instance as its href attribute.
2826
- `Uri::fromMarkdownAnchor` create a new instance from a Markdown code. The first URI found is returned.
2927
- `Uri::fromHtmlAnchor` create a new instance from an HTML code. The first URI found is returned.
3028
- `Uri::toRfc8089` The method will return null if the URI scheme is not the `file` scheme

0 commit comments

Comments
 (0)