mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 00:35:47 +01:00
Allow apostrophes within linkified URL paths.
This commit is contained in:
committed by
Cody Henthorne
parent
c7c3ecb838
commit
86b2819ba6
@@ -27,13 +27,26 @@ object Linkifier {
|
||||
/** Closing brackets that are trimmed only if the URL has no matching opener inside */
|
||||
private val CLOSING_BRACKETS = mapOf(')' to '(', ']' to '[', '}' to '{')
|
||||
|
||||
/**
|
||||
* Characters that always terminate a URL: whitespace (including the unicode variants), bidi/format
|
||||
* controls, and punctuation that is only ever used to quote or wrap a URL.
|
||||
*/
|
||||
private const val URL_TERMINATOR = "\\s\\u0085\\u00A0\\u1680\\u2000-\\u200D\\u2028\\u2029\\u202A-\\u202F\\u205F\\u2066-\\u2069\\u3000\\uFEFF<>\"`|\\\\"
|
||||
|
||||
/**
|
||||
* Characters we treat as definitely-not-part-of-a-URL when extending past the host. Commas and
|
||||
* semicolons are allowed inside a URL, but not when they are acting as a separator before another
|
||||
* obvious URL.
|
||||
*/
|
||||
private const val URL_CHAR =
|
||||
"(?:[^\\s\\u0085\\u00A0\\u1680\\u2000-\\u200D\\u2028\\u2029\\u202A-\\u202F\\u205F\\u2066-\\u2069\\u3000\\uFEFF<>\"'`,;|\\\\]|[,;](?!https?://|www\\.))"
|
||||
private const val URL_CHAR = "(?:[^$URL_TERMINATOR,;]|[,;](?!https?://|www\\.))"
|
||||
|
||||
/**
|
||||
* Like [URL_CHAR], but for the authority (host/port/userinfo) portion of a URL, so it stops at the
|
||||
* first path/query/fragment delimiter. Apostrophes are excluded here so that a possessive like
|
||||
* `https://signal.org's blog` doesn't drag the `'s` into the host, while still allowing them
|
||||
* within a path (see [URL_CHAR]).
|
||||
*/
|
||||
private const val AUTHORITY_CHAR = "(?:[^$URL_TERMINATOR,;'/?#]|[,;](?!https?://|www\\.))"
|
||||
|
||||
/**
|
||||
* A single domain label: letter/digit, optional letter/digit/hyphen body. Used for intermediate
|
||||
@@ -56,7 +69,8 @@ object Linkifier {
|
||||
"(?i)" +
|
||||
"(?:" +
|
||||
// Variant 1: explicit scheme (http or https). User intent is unambiguous; no TLD check.
|
||||
"https?://" + URL_CHAR + "+" +
|
||||
"https?://" + AUTHORITY_CHAR + "+" +
|
||||
"(?:[/?#]" + URL_CHAR + "*)?" +
|
||||
"|" +
|
||||
// Variant 2: starts with www. — also unambiguous; no TLD check.
|
||||
"www\\." + DOMAIN_LABEL + "(?:\\." + DOMAIN_LABEL + ")+" +
|
||||
|
||||
@@ -108,6 +108,17 @@ class LinkifierTest(private val case: Case) {
|
||||
Case("semicolon in url path is preserved", "https://example.com/pricewatch/compare/2290176;2126182/", listOf(web("https://example.com/pricewatch/compare/2290176;2126182/"))),
|
||||
Case("semicolon in url query is preserved", "Go to https://example.com/search?a=1;b=2", listOf(web("https://example.com/search?a=1;b=2"))),
|
||||
|
||||
// ----- apostrophes -----
|
||||
Case("apostrophe in url path is preserved", "https://en.wikipedia.org/wiki/Occam's_razor", listOf(web("https://en.wikipedia.org/wiki/Occam's_razor"))),
|
||||
Case("apostrophe in url path is preserved mid-sentence", "see https://en.wikipedia.org/wiki/Occam's_razor for more", listOf(web("https://en.wikipedia.org/wiki/Occam's_razor"))),
|
||||
Case("apostrophe in bare domain path is preserved", "en.wikipedia.org/wiki/Occam's_razor", listOf(web("en.wikipedia.org/wiki/Occam's_razor", url = "http://en.wikipedia.org/wiki/Occam's_razor"))),
|
||||
Case("apostrophe in url query is preserved", "https://example.com/search?q=Occam's", listOf(web("https://example.com/search?q=Occam's"))),
|
||||
Case("trailing apostrophe is trimmed", "https://example.com/foo' end", listOf(web("https://example.com/foo"))),
|
||||
Case("single-quoted url does not include the quotes", "'https://signal.org'", listOf(web("https://signal.org"))),
|
||||
Case("possessive after schemed host is not part of the url", "https://signal.org's blog is good", listOf(web("https://signal.org"))),
|
||||
Case("possessive after bare domain is not part of the url", "signal.org's blog is good", listOf(web("signal.org", url = "http://signal.org"))),
|
||||
Case("possessive after www domain is not part of the url", "www.signal.org's blog is good", listOf(web("www.signal.org", url = "http://www.signal.org"))),
|
||||
|
||||
// ----- bracket / paren handling -----
|
||||
Case("trailing closing paren without opener is trimmed", "(see https://signal.org)", listOf(web("https://signal.org"))),
|
||||
Case("trailing closing paren with matching opener inside is preserved", "https://en.wikipedia.org/wiki/Foo_(bar) and more", listOf(web("https://en.wikipedia.org/wiki/Foo_(bar)"))),
|
||||
|
||||
Reference in New Issue
Block a user