diff --git a/apps/weblibre/lib/extensions/uri.dart b/apps/weblibre/lib/extensions/uri.dart index f407c008..d8621699 100644 --- a/apps/weblibre/lib/extensions/uri.dart +++ b/apps/weblibre/lib/extensions/uri.dart @@ -47,18 +47,11 @@ extension UriX on Uri { return ''; } - final segments = pathSegments.join('/'); - final buffer = StringBuffer(); - - if (path.startsWith('/')) { - buffer.write('/'); + try { + return Uri.decodeComponent(path); + } on FormatException { + return path; } - buffer.write(segments); - if (path.length > 1 && path.endsWith('/')) { - buffer.write('/'); - } - - return buffer.toString(); } String get displayString { @@ -94,6 +87,10 @@ extension UriStringX on String { if (uri == null || uri.toString() != this) { return this; } - return uri.displayString; + try { + return uri.displayString; + } on FormatException { + return this; + } } } diff --git a/apps/weblibre/lib/features/geckoview/features/contextmenu/extensions/hit_result.dart b/apps/weblibre/lib/features/geckoview/features/contextmenu/extensions/hit_result.dart index 8cf48b7b..4da50d63 100644 --- a/apps/weblibre/lib/features/geckoview/features/contextmenu/extensions/hit_result.dart +++ b/apps/weblibre/lib/features/geckoview/features/contextmenu/extensions/hit_result.dart @@ -21,6 +21,7 @@ import 'dart:convert'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:nullability/nullability.dart'; +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/features/geckoview/features/contextmenu/domain/converters/hit_result_converter.dart'; extension HitResultJson on HitResult { @@ -82,14 +83,15 @@ extension HitResultX on HitResult { const maxTitleLength = 2500; return switch (this) { - UnknownHitResult(src: final src) => src, - ImageSrcHitResult(uri: final uri) => uri, - ImageHitResult(src: final src, title: final title) => - title.isEmpty ? (src.length > maxTitleLength ? 'image' : src) : title!, + UnknownHitResult(src: final src) => src.uriDisplayString, + ImageSrcHitResult(uri: final uri) => uri.uriDisplayString, + ImageHitResult(src: final src, title: final title) => title.isEmpty + ? (src.length > maxTitleLength ? 'image' : src.uriDisplayString) + : title!, VideoHitResult(src: final src, title: final title) => - (title.isEmpty) ? src : title!, + (title.isEmpty) ? src.uriDisplayString : title!, AudioHitResult(src: final src, title: final title) => - (title.isEmpty) ? src : title!, + (title.isEmpty) ? src.uriDisplayString : title!, _ => 'about:blank', }; } diff --git a/apps/weblibre/test/extensions/uri_test.dart b/apps/weblibre/test/extensions/uri_test.dart index 000822ca..a7aca17d 100644 --- a/apps/weblibre/test/extensions/uri_test.dart +++ b/apps/weblibre/test/extensions/uri_test.dart @@ -65,5 +65,34 @@ void main() { 'https://example.com/%ZZ?query=%', ); }); + + test('preserves trailing slash exactly once', () { + final uri = Uri.parse('http://x.com/a/b/'); + + expect(uri.displayString, 'http://x.com/a/b/'); + }); + + test('decodes Cyrillic Reddit URL with trailing slash', () { + final uri = Uri.parse( + 'https://www.reddit.com/r/KafkaFPS/comments/1sn0j5w/' + '%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D0%BD%D1%8C%D1%82%D0%B5' + '_%D1%81%D0%BE%D0%BF%D1%80%D0%BE%D1%82%D0%B8%D0%B2%D0%BB%D1%8F' + '%D1%82%D1%8C%D1%81%D1%8F_%D0%BC%D0%B8%D1%81%D1%82%D0%B5%D1%80' + '_%D0%B0%D0%BD%D0%B4%D0%B5%D1%80%D1%81%D0%BE%D0%BD/', + ); + + expect( + uri.displayString, + 'https://www.reddit.com/r/KafkaFPS/comments/1sn0j5w/' + 'перестаньте_сопротивляться_мистер_андерсон/', + ); + }); + + test('keeps IDN punycode host as-is', () { + expect( + 'https://xn--j1ail.xn--p1ai/'.uriDisplayString, + 'https://xn--j1ail.xn--p1ai/', + ); + }); }); }