Skip to content

Commit 9626bc2

Browse files
committed
feat: support international domains
1 parent 79672bf commit 9626bc2

File tree

4 files changed

+58
-10
lines changed

4 files changed

+58
-10
lines changed

packages/flutter_link_previewer/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A highly customizable Flutter widget for rendering a preview of a URL. It automa
99
## ✨ Features
1010

1111
- 🪄 **Automatic Preview Generation:** Fetches preview data (title, description, image) from the first URL found in a given text.
12+
- 🌐 **International Domain Support:** Handles URLs with localized characters (e.g., Cyrillic, Arabic, Chinese, Japanese, etc.).
1213
- 🎨 **Highly Customizable:** Control colors, padding, border radius, text styles, and more.
1314
- 🖼️ **Smart Image Layout:** Automatically displays images on the side (for square images) or at the bottom (for rectangular images).
1415
- 📐 **Sophisticated Width Calculation:** Aligns with parent content (like chat bubbles) for a visually consistent layout.

packages/flutter_link_previewer/lib/src/link_preview.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class LinkPreview extends StatefulWidget {
2727
this.parentContent,
2828
this.onTap,
2929
this.corsProxy,
30+
this.headers,
3031
this.requestTimeout = const Duration(seconds: 5),
3132
this.userAgent,
3233
this.hideTitle = false,
@@ -82,6 +83,9 @@ class LinkPreview extends StatefulWidget {
8283
/// The CORS proxy to use for fetching the preview data.
8384
final String? corsProxy;
8485

86+
/// The headers to use for fetching the preview data.
87+
final Map<String, String>? headers;
88+
8589
/// The timeout for the request to fetch the preview data.
8690
final Duration? requestTimeout;
8791

@@ -209,6 +213,7 @@ class _LinkPreviewState extends State<LinkPreview>
209213
final value = await getLinkPreviewData(
210214
widget.text,
211215
proxy: widget.corsProxy,
216+
headers: widget.headers,
212217
requestTimeout: widget.requestTimeout,
213218
userAgent: widget.userAgent,
214219
);

packages/flutter_link_previewer/lib/src/utils.dart

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,44 @@ import 'dart:async';
22
import 'dart:convert';
33
import 'dart:typed_data';
44

5+
import 'package:flutter/foundation.dart' show kIsWeb;
56
import 'package:flutter/material.dart' hide Element;
67
import 'package:flutter_chat_core/flutter_chat_core.dart'
78
show LinkPreviewData, ImagePreviewData;
89
import 'package:html/dom.dart' show Document, Element;
910
import 'package:html/parser.dart' as parser show parse;
1011
import 'package:http/http.dart' as http show Request, Client, Response;
12+
import 'package:punycode/punycode.dart' as puny;
1113

1214
import 'types.dart';
1315

1416
String _calculateUrl(String baseUrl, String? proxy) {
17+
var urlToReturn = baseUrl;
18+
19+
final domainRegex = RegExp(r'^(?:(http|https|ftp):\/\/)?([^\/?#]+)');
20+
final match = domainRegex.firstMatch(baseUrl);
21+
22+
if (match != null) {
23+
final originalDomain = match.group(2)!;
24+
25+
final labels = originalDomain.split('.');
26+
if (labels.length <= 10) {
27+
final encodedLabels =
28+
labels.map((label) {
29+
final isAscii = label.runes.every((r) => r < 128);
30+
return isAscii ? label : 'xn--${puny.punycodeEncode(label)}';
31+
}).toList();
32+
33+
final punycodedDomain = encodedLabels.join('.');
34+
urlToReturn = baseUrl.replaceFirst(originalDomain, punycodedDomain);
35+
}
36+
}
37+
1538
if (proxy != null) {
16-
return '$proxy$baseUrl';
39+
return '$proxy$urlToReturn';
1740
}
1841

19-
return baseUrl;
42+
return urlToReturn;
2043
}
2144

2245
String? _getMetaContent(Document document, String propertyValue) {
@@ -155,7 +178,7 @@ Future<String> _getBiggestImageUrl(
155178

156179
Future<http.Response?> _getRedirectedResponse(
157180
Uri uri, {
158-
String? userAgent,
181+
Map<String, String>? headers,
159182
int maxRedirects = 5,
160183
Duration timeout = const Duration(seconds: 5),
161184
http.Client? client,
@@ -164,10 +187,11 @@ Future<http.Response?> _getRedirectedResponse(
164187
var redirectCount = 0;
165188

166189
while (redirectCount < maxRedirects) {
167-
final request =
168-
http.Request('GET', uri)
169-
..followRedirects = false
170-
..headers.addAll({if (userAgent != null) 'User-Agent': userAgent});
190+
final request = http.Request('GET', uri)..followRedirects = false;
191+
192+
if (headers != null) {
193+
request.headers.addAll(headers);
194+
}
171195

172196
final streamedResponse = await httpClient.send(request).timeout(timeout);
173197

@@ -187,6 +211,7 @@ Future<http.Response?> _getRedirectedResponse(
187211
/// Parses provided text and returns [PreviewData] for the first found link.
188212
Future<LinkPreviewData?> getLinkPreviewData(
189213
String text, {
214+
Map<String, String>? headers,
190215
String? proxy,
191216
Duration? requestTimeout,
192217
String? userAgent,
@@ -204,7 +229,7 @@ Future<LinkPreviewData?> getLinkPreviewData(
204229
text.replaceAllMapped(emailRegexp, (match) => '').trim();
205230
if (textWithoutEmails.isEmpty) return null;
206231

207-
final urlRegexp = RegExp(regexLink, caseSensitive: false);
232+
final urlRegexp = RegExp(regexLink, caseSensitive: false, unicode: true);
208233
final matches = urlRegexp.allMatches(textWithoutEmails);
209234
if (matches.isEmpty) return null;
210235

@@ -218,10 +243,26 @@ Future<LinkPreviewData?> getLinkPreviewData(
218243
}
219244
previewDataUrl = _calculateUrl(url, proxy);
220245
final uri = Uri.parse(previewDataUrl);
246+
247+
final defaultHeaders =
248+
kIsWeb
249+
? {
250+
'Access-Control-Allow-Origin': '*',
251+
'Content-Type': 'application/json',
252+
'Accept': '*/*',
253+
}
254+
: {};
255+
256+
final effectiveHeaders = <String, String>{
257+
...defaultHeaders,
258+
'User-Agent': userAgent ?? 'WhatsApp/2',
259+
...?headers,
260+
};
261+
221262
final response = await _getRedirectedResponse(
222263
uri,
264+
headers: effectiveHeaders,
223265
timeout: requestTimeout ?? const Duration(seconds: 5),
224-
userAgent: userAgent ?? 'WhatsApp/2',
225266
);
226267

227268
if (response == null || response.statusCode != 200) {
@@ -307,4 +348,4 @@ const regexImageContentType = r'image\/*';
307348

308349
/// Regex to find all links in the text.
309350
const regexLink =
310-
r'((http|ftp|https):\/\/)?([\w_-]+(?:(?:\.[\w_-]*[a-zA-Z_][\w_-]*)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?[^\.\s]';
351+
r'((http|ftp|https):\/\/)?(([\p{L}\p{N}_-]+)(?:(?:\.([\p{L}\p{N}_-]*[\p{L}_][\p{L}\p{N}_-]*))+))([\p{L}\p{N}.,@?^=%&:/~+#-]*[\p{L}\p{N}@?^=%&/~+#-])?[^\.\s]';

packages/flutter_link_previewer/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies:
1717
flutter_chat_core: ^2.7.0
1818
html: ^0.15.6
1919
http: ^1.4.0
20+
punycode: ^1.0.0
2021
url_launcher: ^6.3.2
2122

2223
dev_dependencies:

0 commit comments

Comments
 (0)