Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/utils/url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {getLinkProps, isAbsoluteUrl, isLinkExternal} from './url';

describe('URL utils check', () => {
test.each([
['https://user:pass@sub.example.com:8080/p/a/t/h?query=string&query2=1#hash', true],
['http://example.net/path', true],
['/p/a/t/h?query=string&query2=1#hash', false],
['/path', false],
['path', false],
])("isAbsoluteUrl('%s') should return '%s'", (url, result) => {
expect(isAbsoluteUrl(url)).toEqual(result);
});

test.each([
[
'https://user:pass@sub.example.com:8080/p/a/t/h?query=string&query2=1#hash',
'example.net',
true,
],
[
'https://user:pass@sub.example.com:8080/p/a/t/h?query=string&query2=1#hash',
'sub.example.com',
false,
],
[
'https://user:pass@sub.example.com:8080/p/a/t/h?query=string&query2=1#hash',
undefined,
true,
],
['http://example.net/path', 'example.net', false],
['http://example.net/path', 'sub.example.com', true],
['http://example.net/path', undefined, true],
['/p/a/t/h?query=string&query2=1#hash', 'example.net', false],
['/p/a/t/h?query=string&query2=1#hash', undefined, false],
['/path', 'example.net', false],
['/path', undefined, false],
['path', 'example.net', false],
['path', undefined, false],
])("isLinkExternal('%s', '%s') should return '%s'", (url, hostname, result) => {
expect(isLinkExternal(url, hostname)).toEqual(result);
});

test.each([
['http://example.net/path', 'example.net', '_blank', {target: '_blank'}],
['http://example.net/path', 'example.net', undefined, {}],
[
'http://example.net/path',
'example.com',
'_blank',
{target: '_blank', rel: 'noopener noreferrer'},
],
[
'http://example.net/path',
'example.com',
undefined,
{target: '_blank', rel: 'noopener noreferrer'},
],
[
'http://example.net/path',
undefined,
'_blank',
{target: '_blank', rel: 'noopener noreferrer'},
],
[
'http://example.net/path',
undefined,
undefined,
{target: '_blank', rel: 'noopener noreferrer'},
],

['/path', 'example.net', '_blank', {target: '_blank'}],
['/path', 'example.net', undefined, {}],
['/path', undefined, '_blank', {target: '_blank'}],
['/path', undefined, undefined, {}],
])("getLinkProps('%s', '%s', '%s') should return '%s'", (url, hostname, target, result) => {
expect(getLinkProps(url, hostname, target)).toEqual(result);
});
});
26 changes: 15 additions & 11 deletions src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,34 @@ import {parse, format} from 'url';

export type Query = Record<string, number | string | null>;

const EXAMPLE_URL = 'https://example.org';
export const EXTERNAL_LINK_PROPS = {target: '_blank', rel: 'noopener noreferrer'};

export function getLinkProps(url: string, hostname?: string, target?: string) {
let linkProps = {target};

if (target === '_blank' || isLinkExternal(url, hostname)) {
if (isLinkExternal(url, hostname)) {
linkProps = {...linkProps, ...EXTERNAL_LINK_PROPS};
}

return linkProps;
}

export function isLinkExternal(url: string, routerHostname?: string) {
if (!routerHostname) {
return true;
}

const {hostname} = parse(url);
export function isAbsoluteUrl(url: string | URL) {
// Using example URL as base for relative links
const urlObj = new URL(url, EXAMPLE_URL);

if (!hostname) {
return false;
}
return (
// Compare url origin with example and check that original url was not example one
urlObj.origin !== EXAMPLE_URL || (typeof url === 'string' && url.startsWith(EXAMPLE_URL))
);
}

return getNonLocaleHostName(hostname) !== getNonLocaleHostName(routerHostname);
export function isLinkExternal(url: string, routerHostname?: string) {
return (
isAbsoluteUrl(url) &&
getNonLocaleHostName(new URL(url).hostname) !== getNonLocaleHostName(routerHostname ?? '')
);
}

export function getNonLocaleHostName(hostname: string) {
Expand Down