diff --git a/src/Url/UrlTransformer.php b/src/Url/UrlTransformer.php
new file mode 100644
index 0000000..99db928
--- /dev/null
+++ b/src/Url/UrlTransformer.php
@@ -0,0 +1,32 @@
+all() as $mapping ) {
+ $transformed = $this->replaceMapping( $transformed, $mapping );
+ }
+
+ return $transformed;
+ }
+
+ private function replaceMapping( string $value, UrlMapping $mapping ): string {
+ $source = $mapping->sourceUrl();
+ $destination = $mapping->destinationUrl();
+
+ $replacements = array(
+ $source => $destination,
+ str_replace( '&', '&', $source ) => str_replace( '&', '&', $destination ),
+ $this->toProtocolRelative( $source ) => $this->toProtocolRelative( $destination ),
+ );
+
+ return strtr( $value, $replacements );
+ }
+
+ private function toProtocolRelative( string $url ): string {
+ return preg_replace( '#^https?:#', '', $url ) ?? $url;
+ }
+}
diff --git a/tests/Unit/Url/UrlTransformerTest.php b/tests/Unit/Url/UrlTransformerTest.php
new file mode 100644
index 0000000..1d3f71d
--- /dev/null
+++ b/tests/Unit/Url/UrlTransformerTest.php
@@ -0,0 +1,84 @@
+transformString( 'Visit https://example.test/about for details.', $mappings )
+ );
+ }
+
+ public function test_it_rewrites_html_attribute_urls(): void {
+ $transformer = new UrlTransformer();
+ $mappings = new UrlMappingCollection(
+ array(
+ new UrlMapping( 'https://example.test', 'https://staging.example.test' ),
+ )
+ );
+
+ self::assertSame(
+ '
',
+ $transformer->transformString(
+ '
',
+ $mappings
+ )
+ );
+ }
+
+ public function test_it_rewrites_escaped_urls(): void {
+ $transformer = new UrlTransformer();
+ $mappings = new UrlMappingCollection(
+ array(
+ new UrlMapping( 'https://example.test/path?a=1&b=2', 'https://staging.example.test/path?a=1&b=2' ),
+ )
+ );
+
+ self::assertSame(
+ 'https://staging.example.test/path?a=1&b=2',
+ $transformer->transformString( 'https://example.test/path?a=1&b=2', $mappings )
+ );
+ }
+
+ public function test_it_rewrites_protocol_relative_urls(): void {
+ $transformer = new UrlTransformer();
+ $mappings = new UrlMappingCollection(
+ array(
+ new UrlMapping( 'https://example.test', 'https://staging.example.test' ),
+ )
+ );
+
+ self::assertSame(
+ '//staging.example.test/uploads/file.pdf',
+ $transformer->transformString( '//example.test/uploads/file.pdf', $mappings )
+ );
+ }
+
+ public function test_it_supports_multiple_mappings(): void {
+ $transformer = new UrlTransformer();
+ $mappings = new UrlMappingCollection(
+ array(
+ new UrlMapping( 'https://example.test', 'https://staging.example.test' ),
+ new UrlMapping( 'https://cdn.example.test', 'https://cdn.staging.example.test' ),
+ )
+ );
+
+ self::assertSame(
+ 'https://staging.example.test https://cdn.staging.example.test/image.jpg',
+ $transformer->transformString( 'https://example.test https://cdn.example.test/image.jpg', $mappings )
+ );
+ }
+}