Skip to content

Commit 2a89808

Browse files
committed
WIP escaping & chars in link text
1 parent e9cb46d commit 2a89808

File tree

6 files changed

+61
-39
lines changed

6 files changed

+61
-39
lines changed

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,15 @@
4040
"pnpm": ">=10.10.0"
4141
},
4242
"keywords": [
43-
"auto",
4443
"link",
44+
"linkify",
4545
"autolink",
4646
"url",
47-
"urls",
48-
"anchor"
47+
"anchor",
48+
"email",
49+
"hashtags",
50+
"mentions",
51+
"phone"
4952
],
5053
"author": "Gregory Jacobs <[email protected]>",
5154
"license": "MIT",

src/match/url-match.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,21 @@ export class UrlMatch extends AbstractMatch {
186186
if (this.stripTrailingSlash) {
187187
anchorText = removeTrailingSlash(anchorText); // remove trailing slash, if there is one
188188
}
189+
190+
// For any '&' char in the URL, this can be interpreted by the browser
191+
// as beginning a new HTML entity, which is not the intention. For
192+
// example, the '&not' in 'http://google.com/search=hi&not=1' will be
193+
// replaced with the '¬' char. Hence, we HTML-escape the '&' chars to
194+
// '&amp;' for display purposes
195+
anchorText = anchorText.replace(/&/g, '&amp;');
196+
197+
// And finally, decodePercentEncoding if requested. This must happen
198+
// after the above '&'->'&amp;' replacements or we'll end up double-encoding
199+
// '&' chars generated from the removePercentEncoding() function itself
189200
if (this.decodePercentEncoding) {
190201
anchorText = removePercentEncoding(anchorText);
191202
}
203+
192204
return anchorText;
193205
}
194206
}

tests/autolinker-url-other.spec.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ describe('General URL Matching behavior (other) >', () => {
165165
"Here's an example: http://example.com/foo.php?bar[]=1&bar[]=2&bar[]=3"
166166
);
167167
expect(result).to.equal(
168-
`Here's an example: <a href="http://example.com/foo.php?bar[]=1&bar[]=2&bar[]=3">example.com/foo.php?bar[]=1&bar[]=2&bar[]=3</a>`
168+
`Here's an example: <a href="http://example.com/foo.php?bar[]=1&bar[]=2&bar[]=3">example.com/foo.php?bar[]=1&amp;bar[]=2&amp;bar[]=3</a>`
169169
);
170170
});
171171

@@ -177,7 +177,7 @@ describe('General URL Matching behavior (other) >', () => {
177177
"Here's an example: [http://example.com/foo.php?bar[]=1&bar[]=2&bar[]=3]"
178178
);
179179
expect(result).to.equal(
180-
`Here's an example: [<a href="http://example.com/foo.php?bar[]=1&bar[]=2&bar[]=3">example.com/foo.php?bar[]=1&bar[]=2&bar[]=3</a>]`
180+
`Here's an example: [<a href="http://example.com/foo.php?bar[]=1&bar[]=2&bar[]=3">example.com/foo.php?bar[]=1&amp;bar[]=2&amp;bar[]=3</a>]`
181181
);
182182
});
183183
});
@@ -197,7 +197,7 @@ describe('General URL Matching behavior (other) >', () => {
197197
"Here's an example: https://gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&action=edit"
198198
);
199199
expect(result).to.equal(
200-
`Here's an example: <a href="https://gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&action=edit">gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&action=edit</a>`
200+
`Here's an example: <a href="https://gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&action=edit">gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&amp;action=edit</a>`
201201
);
202202
});
203203

@@ -207,7 +207,7 @@ describe('General URL Matching behavior (other) >', () => {
207207
"Here's an example: https://gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&action=edit"
208208
);
209209
expect(result).to.equal(
210-
`Here's an example: <a href="https://gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&action=edit">gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&action=edit</a>`
210+
`Here's an example: <a href="https://gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&action=edit">gohub.sharepoint.com/example/doc.aspx?sourcedoc={foobar}&amp;action=edit</a>`
211211
);
212212
});
213213

@@ -237,7 +237,7 @@ describe('General URL Matching behavior (other) >', () => {
237237
'Check out the image at http://server.com/template?fmt=jpeg&$base=700.'
238238
);
239239
expect(result).to.equal(
240-
'Check out the image at <a href="http://server.com/template?fmt=jpeg&$base=700">server.com/template?fmt=jpeg&$base=700</a>.'
240+
'Check out the image at <a href="http://server.com/template?fmt=jpeg&$base=700">server.com/template?fmt=jpeg&amp;$base=700</a>.'
241241
);
242242
});
243243

@@ -255,7 +255,7 @@ describe('General URL Matching behavior (other) >', () => {
255255
'Twitter search for bob smith https://api.twitter.com/1.1/users/search.json?count=20&q=Bob+*+Smith'
256256
);
257257
expect(result).to.equal(
258-
'Twitter search for bob smith <a href="https://api.twitter.com/1.1/users/search.json?count=20&q=Bob+*+Smith">api.twitter.com/1.1/users/search.json?count=20&q=Bob+*+Smith</a>'
258+
'Twitter search for bob smith <a href="https://api.twitter.com/1.1/users/search.json?count=20&q=Bob+*+Smith">api.twitter.com/1.1/users/search.json?count=20&amp;q=Bob+*+Smith</a>'
259259
);
260260
});
261261

@@ -264,7 +264,7 @@ describe('General URL Matching behavior (other) >', () => {
264264
'Test caret url: https://sourcegraph.yelpcorp.com/search?q=repo:^services&patternType=literal'
265265
);
266266
expect(result).to.equal(
267-
'Test caret url: <a href="https://sourcegraph.yelpcorp.com/search?q=repo:^services&patternType=literal">sourcegraph.yelpcorp.com/search?q=repo:^services&patternType=literal</a>'
267+
'Test caret url: <a href="https://sourcegraph.yelpcorp.com/search?q=repo:^services&patternType=literal">sourcegraph.yelpcorp.com/search?q=repo:^services&amp;patternType=literal</a>'
268268
);
269269
});
270270

@@ -284,12 +284,19 @@ describe('General URL Matching behavior (other) >', () => {
284284
);
285285
});
286286

287+
it('when & chars are included in the URL, they should be changed into &amp; entities for displaying the link so as to not be interpreted as starting a new HTML entity itself (https://github.com/gregjacobs/Autolinker.js/issues/392)', () => {
288+
const result = autolinker.link('Check out https://tempuri.org?a=1&not=a');
289+
expect(result).to.equal(
290+
'Check out <a href="https://tempuri.org?a=1&not=a">tempuri.org?a=1&amp;not=a</a>'
291+
);
292+
});
293+
287294
it('should include [ and ] in URLs with query strings', () => {
288295
const result = autolinker.link(
289296
'Go to https://example.com/api/export/873/?a[]=10&a[]=9&a[]=8&a[]=7&a[]=6 today'
290297
);
291298
expect(result).to.equal(
292-
'Go to <a href="https://example.com/api/export/873/?a[]=10&a[]=9&a[]=8&a[]=7&a[]=6">example.com/api/export/873/?a[]=10&a[]=9&a[]=8&a[]=7&a[]=6</a> today'
299+
'Go to <a href="https://example.com/api/export/873/?a[]=10&a[]=9&a[]=8&a[]=7&a[]=6">example.com/api/export/873/?a[]=10&amp;a[]=9&amp;a[]=8&amp;a[]=7&amp;a[]=6</a> today'
293300
);
294301
});
295302

@@ -425,7 +432,7 @@ describe('General URL Matching behavior (other) >', () => {
425432
Hashes too <a href="http://yahoo.com:8000/#some-link">yahoo.com:8000/#some-link</a>.
426433
Sometimes you need a lot of things in the URL like <a href="https://abc123def.org/path1/2path?param1=value1#hash123z">abc123def.org/path1/2path?param1=value1#hash123z</a>
427434
Do you see the need for dashes in these things too <a href="https://abc-def.org/his-path/?the-param=the-value#the-hash">abc-def.org/his-path/?the-param=the-value#the-hash</a>?
428-
There's a time for lots and lots of special characters like in <a href="https://abc123def.org/-+&@#/%=~_()|'$*[]?!:,.;/?param1=value-+&@#/%=~_()|'$*[]?!:,.;#hash-+&@#/%=~_()|'$*[]?!:,.;z">abc123def.org/-+&@#/%=~_()|'$*[]?!:,.;/?param1=value-+&@#/%=~_()|'$*[]?!:,.;#hash-+&@#/%=~_()|'$*[]?!:,.;z</a>
435+
There's a time for lots and lots of special characters like in <a href="https://abc123def.org/-+&@#/%=~_()|'$*[]?!:,.;/?param1=value-+&@#/%=~_()|'$*[]?!:,.;#hash-+&@#/%=~_()|'$*[]?!:,.;z">abc123def.org/-+&amp;@#/%=~_()|'$*[]?!:,.;/?param1=value-+&amp;@#/%=~_()|'$*[]?!:,.;#hash-+&amp;@#/%=~_()|'$*[]?!:,.;z</a>
429436
Don't forget about good times with unicode <a href="https://ru.wikipedia.org/wiki/Кириллица?Кириллица=1#Кириллица">ru.wikipedia.org/wiki/Кириллица?Кириллица=1#Кириллица</a>
430437
and this unicode <a href="http://россия.рф">россия.рф</a>
431438
along with punycode <a href="http://xn--d1acufc.xn--p1ai">xn--d1acufc.xn--p1ai</a>
@@ -441,7 +448,7 @@ describe('General URL Matching behavior (other) >', () => {
441448
);
442449

443450
expect(result).to.equal(
444-
'<a href="https://example.com/api/path?apikey={API_Key}&message=Test&[email protected]&department=someid123&subject=Some_Subject&[email protected]&is_html_message=Y">example.com/api/path?apikey={API_Key}&message=Test&[email protected]&department=someid123&subject=Some_Subject&[email protected]&is_html_message=Y</a>'
451+
'<a href="https://example.com/api/path?apikey={API_Key}&message=Test&[email protected]&department=someid123&subject=Some_Subject&[email protected]&is_html_message=Y">example.com/api/path?apikey={API_Key}&amp;message=Test&amp;[email protected]&amp;department=someid123&amp;subject=Some_Subject&amp;[email protected]&amp;is_html_message=Y</a>'
445452
);
446453
});
447454
});

tests/autolinker-url-scheme.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe(`Matching scheme-prefixed URLs (i.e. URLs starting with 'http://' or 's
174174
'https://gitlab.example.com/search?utf8=✓&search=mysearch&group_id=&project_id=42&search_code=true&repository_ref=master'
175175
);
176176
expect(result).to.equal(
177-
'<a href="https://gitlab.example.com/search?utf8=✓&search=mysearch&group_id=&project_id=42&search_code=true&repository_ref=master">gitlab.example.com/search?utf8=✓&search=mysearch&group_id=&project_id=42&search_code=true&repository_ref=master</a>'
177+
'<a href="https://gitlab.example.com/search?utf8=✓&search=mysearch&group_id=&project_id=42&search_code=true&repository_ref=master">gitlab.example.com/search?utf8=✓&amp;search=mysearch&amp;group_id=&amp;project_id=42&amp;search_code=true&amp;repository_ref=master</a>'
178178
);
179179
});
180180

tests/autolinker.spec.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -677,25 +677,6 @@ describe('Autolinker', function () {
677677
);
678678
});
679679

680-
it('should handle HTML entities like &nbsp; within a non-autolinked part of a text node, properly appending it to the output', function () {
681-
const html =
682-
'Joe went to yahoo.com and used HTML&nbsp;entities like &gt; and &lt; google.com';
683-
684-
const result = autolinker.link(html);
685-
expect(result).to.equal(
686-
'Joe went to <a href="http://yahoo.com">yahoo.com</a> and used HTML&nbsp;entities like &gt; and &lt; <a href="http://google.com">google.com</a>'
687-
);
688-
});
689-
690-
it('should handle &amp; inside a url and not ignore it', function () {
691-
const html = '<p>Joe went to example.com?arg=1&amp;arg=2&amp;arg=3</p>';
692-
693-
const result = autolinker.link(html);
694-
expect(result).to.equal(
695-
'<p>Joe went to <a href="http://example.com?arg=1&arg=2&arg=3">example.com?arg=1&amp;arg=2&amp;arg=3</a></p>'
696-
);
697-
});
698-
699680
it('should handle line breaks inside an HTML tag, not accidentally autolinking a URL within the tag', function () {
700681
const html =
701682
'<a href="http://close.io/" style="font-family: Helvetica,\nArial">http://close.io</a>';
@@ -750,6 +731,25 @@ describe('Autolinker', function () {
750731
const result = autolinker.link('&amp;');
751732
expect(result).to.equal('&amp;');
752733
});
734+
735+
it('should handle HTML entities like &nbsp; within a non-autolinked part of a text node, properly appending it to the output', function () {
736+
const html =
737+
'Joe went to yahoo.com and used HTML&nbsp;entities like &gt; and &lt; google.com';
738+
739+
const result = autolinker.link(html);
740+
expect(result).to.equal(
741+
'Joe went to <a href="http://yahoo.com">yahoo.com</a> and used HTML&nbsp;entities like &gt; and &lt; <a href="http://google.com">google.com</a>'
742+
);
743+
});
744+
745+
it('should handle &amp; inside a url and not ignore it', function () {
746+
const html = '<p>Joe went to example.com?arg=1&amp;arg=2&amp;arg=3</p>';
747+
748+
const result = autolinker.link(html);
749+
expect(result).to.equal(
750+
'<p>Joe went to <a href="http://example.com?arg=1&arg=2&arg=3">example.com?arg=1&amp;arg=2&amp;arg=3</a></p>'
751+
);
752+
});
753753
});
754754

755755
describe('`newWindow` option', function () {
@@ -1800,15 +1800,15 @@ describe('Autolinker', function () {
18001800
const testCases = {
18011801
schemeUrl: {
18021802
unlinked: 'http://google.com/path?param1=value1&param2=value2#hash',
1803-
linked: '<a href="http://google.com/path?param1=value1&param2=value2#hash">http://google.com/path?param1=value1&param2=value2#hash</a>',
1803+
linked: '<a href="http://google.com/path?param1=value1&param2=value2#hash">http://google.com/path?param1=value1&amp;param2=value2#hash</a>',
18041804
},
18051805
tldUrl: {
18061806
unlinked: 'google.com/path?param1=value1&param2=value2#hash',
1807-
linked: '<a href="http://google.com/path?param1=value1&param2=value2#hash">google.com/path?param1=value1&param2=value2#hash</a>',
1807+
linked: '<a href="http://google.com/path?param1=value1&param2=value2#hash">google.com/path?param1=value1&amp;param2=value2#hash</a>',
18081808
},
18091809
ipV4Url: {
18101810
unlinked: '192.168.0.1/path?param1=value1&param2=value2#hash',
1811-
linked: '<a href="http://192.168.0.1/path?param1=value1&param2=value2#hash">192.168.0.1/path?param1=value1&param2=value2#hash</a>',
1811+
linked: '<a href="http://192.168.0.1/path?param1=value1&param2=value2#hash">192.168.0.1/path?param1=value1&amp;param2=value2#hash</a>',
18121812
},
18131813
email: {
18141814
unlinked: '[email protected]',

tests/util/generate-url-combination-tests.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function generateUrlCombinationTests({
7575
input: `${origin}?param=1&param-2=2`,
7676
description: 'long query string',
7777
expectedHref: `${expectedOrigin}?param=1&param-2=2`,
78-
expectedAnchorText: `${hierPart}?param=1&param-2=2`,
78+
expectedAnchorText: `${hierPart}?param=1&amp;param-2=2`,
7979
autolinker,
8080
},
8181
{
@@ -89,7 +89,7 @@ export function generateUrlCombinationTests({
8989
input: `${origin}#param1=a&param2=b`,
9090
description: 'hash params',
9191
expectedHref: `${expectedOrigin}#param1=a&param2=b`,
92-
expectedAnchorText: `${hierPart}#param1=a&param2=b`,
92+
expectedAnchorText: `${hierPart}#param1=a&amp;param2=b`,
9393
autolinker,
9494
},
9595
{
@@ -110,7 +110,7 @@ export function generateUrlCombinationTests({
110110
input: `${origin}/-+&@#/%=~_()|'$*[]?!:,.;/?param1=value-+&@#/%=~_()|'$*[]?!:,.;#hash-+&@#/%=~_()|'$*[]?!:,.;z`,
111111
description: 'all special chars in the URL',
112112
expectedHref: `${expectedOrigin}/-+&@#/%=~_()|'$*[]?!:,.;/?param1=value-+&@#/%=~_()|'$*[]?!:,.;#hash-+&@#/%=~_()|'$*[]?!:,.;z`,
113-
expectedAnchorText: `${hierPart}/-+&@#/%=~_()|'$*[]?!:,.;/?param1=value-+&@#/%=~_()|'$*[]?!:,.;#hash-+&@#/%=~_()|'$*[]?!:,.;z`,
113+
expectedAnchorText: `${hierPart}/-+&amp;@#/%=~_()|'$*[]?!:,.;/?param1=value-+&amp;@#/%=~_()|'$*[]?!:,.;#hash-+&amp;@#/%=~_()|'$*[]?!:,.;z`,
114114
autolinker,
115115
},
116116
{

0 commit comments

Comments
 (0)