Skip to content

Commit

Permalink
Export functions based on return type
Browse files Browse the repository at this point in the history
  • Loading branch information
fregante committed Aug 24, 2024
1 parent ef991e9 commit f679537
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 105 deletions.
47 changes: 24 additions & 23 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@ export interface Options {
*/
readonly attributes?: HTMLAttributes;

/**
The format of the generated content.
`'string'` will return it as a flat string like `'Visit <a href="https://example.com">https://example.com</a>'`.
`'dom'` will return it as a `DocumentFragment` ready to be appended in a DOM safely, like `DocumentFragment(TextNode('Visit '), HTMLAnchorElement('https://example.com'))`. This type only works in the browser.
*/
readonly type?: 'string' | 'dom';

/**
Set a custom HTML value for the link.
Expand All @@ -34,14 +25,12 @@ export interface Options {

}

export interface TypeDomOptions extends Options {
readonly type: 'dom';
}

/**
Linkify URLs in a string.
Linkify URLs in a string, returns an HTML string.
@param A string with URLs to linkify.
@param string - A string with URLs to linkify.
@returns An HTML string like `'Visit <a href="https://example.com">https://example.com</a>'`.
@example
```
Expand All @@ -56,9 +45,25 @@ linkifyUrls('See https://sindresorhus.com', {
}
});
//=> 'See <a href="https://sindresorhus.com" class="unicorn" one="1" foo multiple="a b">https://sindresorhus.com</a>'
```
*/
export function linkifyUrlsToHtml(
string: string,
options?: Options
): string;

/**
Linkify URLs in a string, returns a `DocumentFragment`.
@param A string with URLs to linkify.
@returns a `DocumentFragment` ready to be appended in a DOM safely, like `DocumentFragment(TextNode('Visit '), HTMLAnchorElement('https://example.com'))`. This type only works in the browser.
@example
```
import {linkifyUrlsToDom} from 'linkify-urls';
// In the browser
const fragment = linkifyUrls('See https://sindresorhus.com', {
const fragment = linkifyUrlsToDom('See https://sindresorhus.com', {
type: 'dom',
attributes: {
class: 'unicorn',
Expand All @@ -67,13 +72,9 @@ const fragment = linkifyUrls('See https://sindresorhus.com', {
document.body.appendChild(fragment);
```
*/
export default function linkifyUrls(
string: string,
options: TypeDomOptions
): DocumentFragment;
export default function linkifyUrls(
export function linkifyUrlsToDom(
string: string,
options?: Options
): string;
): DocumentFragment;

export {HTMLAttributes} from 'create-html-element';
34 changes: 10 additions & 24 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import createHtmlElement from 'create-html-element';
const urlRegex = () => (/((?<!\+)https?:\/\/(?:www\.)?(?:[-\w.]+?[.@][a-zA-Z\d]{2,}|localhost)(?:[-\w.:%+~#*$!?&/=@]*?(?:,(?!\s))*?)*)/g);

// Get `<a>` element as string
const linkify = (href, options) => createHtmlElement({
const linkify = (href, options = {}) => createHtmlElement({
name: 'a',
attributes: {
href: '',
Expand All @@ -23,12 +23,16 @@ const isTruncated = (url, peek) =>
url.endsWith('...') // `...` is a matched by the URL regex
|| peek.startsWith('…'); // `…` can follow the match

const getAsString = (string, options) => string.replace(urlRegex(), (url, _, offset) =>
(isTruncated(url, string.charAt(offset + url.length)))
? url // Don't linkify truncated URLs
: linkify(url, options));
export function linkifyUrlsToHtml(string, options) {
const replacer = (url, _, offset) =>
isTruncated(url, string.charAt(offset + url.length))
? url // Don't linkify truncated URLs
: linkify(url, options);

const getAsDocumentFragment = (string, options) => {
return string.replace(urlRegex(), replacer);
}

export function linkifyUrlsToDom(string, options) {
const fragment = document.createDocumentFragment();
const parts = string.split(urlRegex());

Expand All @@ -42,22 +46,4 @@ const getAsDocumentFragment = (string, options) => {
}

return fragment;
};

export default function linkifyUrls(string, options) {
options = {
attributes: {},
type: 'string',
...options,
};

if (options.type === 'string') {
return getAsString(string, options);
}

if (options.type === 'dom') {
return getAsDocumentFragment(string, options);
}

throw new TypeError('The type option must be either `dom` or `string`');
}
15 changes: 6 additions & 9 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {expectType} from 'tsd';
import linkifyUrls from './index.js';
import {linkifyUrlsToHtml, linkifyUrlsToDom} from './index.js';

expectType<string>(
linkifyUrls('See https://sindresorhus.com', {
linkifyUrlsToHtml('See https://sindresorhus.com', {
attributes: {
class: 'unicorn',
one: 1,
Expand All @@ -12,26 +12,23 @@ expectType<string>(
}),
);
expectType<string>(
linkifyUrls('See https://sindresorhus.com', {
linkifyUrlsToHtml('See https://sindresorhus.com', {
value: 'foo',
}),
);
expectType<string>(
linkifyUrls('See https://sindresorhus.com/foo', {
linkifyUrlsToHtml('See https://sindresorhus.com/foo', {
value: url => {
expectType<string>(url);
return url;
},
}),
);
expectType<string>(
linkifyUrls('See https://sindresorhus.com/foo', {
type: 'string',
}),
linkifyUrlsToHtml('See https://sindresorhus.com/foo'),
);

const fragment = linkifyUrls('See https://sindresorhus.com', {
type: 'dom',
const fragment = linkifyUrlsToDom('See https://sindresorhus.com', {
attributes: {
class: 'unicorn',
},
Expand Down
85 changes: 36 additions & 49 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {URL} from 'node:url';
import test from 'ava';
import jsdom from 'jsdom';
import linkifyUrls from './index.js';
import {linkifyUrlsToDom, linkifyUrlsToHtml} from './index.js';

const dom = new jsdom.JSDOM();
globalThis.window = dom.window;
Expand Down Expand Up @@ -29,12 +29,12 @@ const html = dom => {

test('main', t => {
t.is(
linkifyUrls('See https://sindresorhus.com and https://github.com/sindresorhus/got'),
linkifyUrlsToHtml('See https://sindresorhus.com and https://github.com/sindresorhus/got'),
'See <a href="https://sindresorhus.com">https://sindresorhus.com</a> and <a href="https://github.com/sindresorhus/got">https://github.com/sindresorhus/got</a>',
);

t.is(
linkifyUrls('See https://sindresorhus.com', {
linkifyUrlsToHtml('See https://sindresorhus.com', {
attributes: {
class: 'unicorn',
target: '_blank',
Expand All @@ -44,14 +44,14 @@ test('main', t => {
);

t.is(
linkifyUrls('[![Build Status](https://travis-ci.org/sindresorhus/caprine.svg?branch=main)](https://travis-ci.org/sindresorhus/caprine)'),
linkifyUrlsToHtml('[![Build Status](https://travis-ci.org/sindresorhus/caprine.svg?branch=main)](https://travis-ci.org/sindresorhus/caprine)'),
'[![Build Status](<a href="https://travis-ci.org/sindresorhus/caprine.svg?branch=main">https://travis-ci.org/sindresorhus/caprine.svg?branch=main</a>)](<a href="https://travis-ci.org/sindresorhus/caprine">https://travis-ci.org/sindresorhus/caprine</a>)',
);
});

test('supports boolean and non-string attribute values', t => {
t.is(
linkifyUrls('https://sindresorhus.com', {
linkifyUrlsToHtml('https://sindresorhus.com', {
attributes: {
foo: true,
bar: false,
Expand All @@ -64,15 +64,12 @@ test('supports boolean and non-string attribute values', t => {

test('DocumentFragment support', t => {
t.is(
html(linkifyUrls('See https://sindresorhus.com and https://github.com/sindresorhus/got', {
type: 'dom',
})),
html(linkifyUrlsToDom('See https://sindresorhus.com and https://github.com/sindresorhus/got')),
html(domify('See <a href="https://sindresorhus.com">https://sindresorhus.com</a> and <a href="https://github.com/sindresorhus/got">https://github.com/sindresorhus/got</a>')),
);

t.is(
html(linkifyUrls('See https://sindresorhus.com', {
type: 'dom',
html(linkifyUrlsToDom('See https://sindresorhus.com', {
attributes: {
class: 'unicorn',
target: '_blank',
Expand All @@ -82,110 +79,100 @@ test('DocumentFragment support', t => {
);

t.is(
html(linkifyUrls('[![Build Status](https://travis-ci.org/sindresorhus/caprine.svg?branch=main)](https://travis-ci.org/sindresorhus/caprine)', {
type: 'dom',
})),
html(linkifyUrlsToDom('[![Build Status](https://travis-ci.org/sindresorhus/caprine.svg?branch=main)](https://travis-ci.org/sindresorhus/caprine)')),
html(domify('[![Build Status](<a href="https://travis-ci.org/sindresorhus/caprine.svg?branch=main">https://travis-ci.org/sindresorhus/caprine.svg?branch=main</a>)](<a href="https://travis-ci.org/sindresorhus/caprine">https://travis-ci.org/sindresorhus/caprine</a>)')),
);
});

test('escapes the URL', t => {
t.is(linkifyUrls('https://mysite.com/?emp=1&amp=2'), '<a href="https://mysite.com/?emp=1&amp;amp=2">https://mysite.com/?emp=1&amp;amp=2</a>');
t.is(linkifyUrlsToHtml('https://mysite.com/?emp=1&amp=2'), '<a href="https://mysite.com/?emp=1&amp;amp=2">https://mysite.com/?emp=1&amp;amp=2</a>');
});

test('supports `@` in the URL path', t => {
t.is(linkifyUrls('https://sindresorhus.com/@foo'), '<a href="https://sindresorhus.com/@foo">https://sindresorhus.com/@foo</a>');
t.is(linkifyUrlsToHtml('https://sindresorhus.com/@foo'), '<a href="https://sindresorhus.com/@foo">https://sindresorhus.com/@foo</a>');
});

test('supports `#!` in the URL path', t => {
t.is(linkifyUrls('https://twitter.com/#!/sindresorhus'), '<a href="https://twitter.com/#!/sindresorhus">https://twitter.com/#!/sindresorhus</a>');
t.is(linkifyUrlsToHtml('https://twitter.com/#!/sindresorhus'), '<a href="https://twitter.com/#!/sindresorhus">https://twitter.com/#!/sindresorhus</a>');
});

test('supports *$ in the URL path', t => {
t.is(linkifyUrls('https://sindresorhus.com/#1_*'), '<a href="https://sindresorhus.com/#1_*">https://sindresorhus.com/#1_*</a>');
t.is(linkifyUrls('https://sindresorhus.com/#1_$'), '<a href="https://sindresorhus.com/#1_$">https://sindresorhus.com/#1_$</a>');
t.is(linkifyUrlsToHtml('https://sindresorhus.com/#1_*'), '<a href="https://sindresorhus.com/#1_*">https://sindresorhus.com/#1_*</a>');
t.is(linkifyUrlsToHtml('https://sindresorhus.com/#1_$'), '<a href="https://sindresorhus.com/#1_$">https://sindresorhus.com/#1_$</a>');
});

test('supports `,` in the URL path, but not at the end', t => {
t.is(linkifyUrls('https://sindresorhus.com/?id=foo,bar'), '<a href="https://sindresorhus.com/?id=foo,bar">https://sindresorhus.com/?id=foo,bar</a>');
t.is(linkifyUrls('https://sindresorhus.com/?id=foo, bar'), '<a href="https://sindresorhus.com/?id=foo">https://sindresorhus.com/?id=foo</a>, bar');
t.is(linkifyUrlsToHtml('https://sindresorhus.com/?id=foo,bar'), '<a href="https://sindresorhus.com/?id=foo,bar">https://sindresorhus.com/?id=foo,bar</a>');
t.is(linkifyUrlsToHtml('https://sindresorhus.com/?id=foo, bar'), '<a href="https://sindresorhus.com/?id=foo">https://sindresorhus.com/?id=foo</a>, bar');
});

test('supports `value` option', t => {
t.is(linkifyUrls('See https://github.com/sindresorhus.com/linkify-urls for a solution', {
t.is(linkifyUrlsToHtml('See https://github.com/sindresorhus.com/linkify-urls for a solution', {
type: 'string',
value: 0,
}), 'See <a href="https://github.com/sindresorhus.com/linkify-urls">0</a> for a solution');
});

test('supports `value` option as function', t => {
t.is(linkifyUrls('See https://github.com/sindresorhus.com/linkify-urls for a solution', {
t.is(linkifyUrlsToHtml('See https://github.com/sindresorhus.com/linkify-urls for a solution', {
value: url => new URL(url).hostname,
}), 'See <a href="https://github.com/sindresorhus.com/linkify-urls">github.com</a> for a solution');
});

test.failing('skips the trailing period', t => {
t.is(linkifyUrls('Visit https://fregante.com.'), 'Visit <a href="https://fregante.com">https://fregante.com</a>.');
t.is(linkifyUrlsToHtml('Visit https://fregante.com.'), 'Visit <a href="https://fregante.com">https://fregante.com</a>.');
});

test('skips URLs preceded by a `+` sign', t => {
const fixture = 'git+https://github.com/sindresorhus/ava';
t.is(linkifyUrls(fixture), fixture);
t.is(linkifyUrlsToHtml(fixture), fixture);
});

test('supports username in url', t => {
t.is(linkifyUrls('https://[email protected]/@foo'), '<a href="https://[email protected]/@foo">https://[email protected]/@foo</a>');
t.is(linkifyUrlsToHtml('https://[email protected]/@foo'), '<a href="https://[email protected]/@foo">https://[email protected]/@foo</a>');
});

test('supports a URL with a subdomain', t => {
t.is(linkifyUrls('https://docs.google.com'), '<a href="https://docs.google.com">https://docs.google.com</a>');
t.is(linkifyUrlsToHtml('https://docs.google.com'), '<a href="https://docs.google.com">https://docs.google.com</a>');
});

test('skips email addresses', t => {
t.is(linkifyUrls('[email protected]'), '[email protected]');
t.is(linkifyUrls('[email protected]'), '[email protected]');
t.is(linkifyUrls('[email protected]'), '[email protected]');
t.is(linkifyUrlsToHtml('[email protected]'), '[email protected]');
t.is(linkifyUrlsToHtml('[email protected]'), '[email protected]');
t.is(linkifyUrlsToHtml('[email protected]'), '[email protected]');
});

test('supports localhost URLs', t => {
t.is(linkifyUrls('https://localhost'), '<a href="https://localhost">https://localhost</a>');
t.is(linkifyUrls('https://localhost/foo/bar'), '<a href="https://localhost/foo/bar">https://localhost/foo/bar</a>');
t.is(linkifyUrlsToHtml('https://localhost'), '<a href="https://localhost">https://localhost</a>');
t.is(linkifyUrlsToHtml('https://localhost/foo/bar'), '<a href="https://localhost/foo/bar">https://localhost/foo/bar</a>');
});

test('skips truncated URLs', t => {
t.is(linkifyUrls('https://github.com/sindresorhus/linkify-…'), 'https://github.com/sindresorhus/linkify-…');
t.is(linkifyUrls('https://github.com/sindresorhus/linkify-… and https://github.com/sindresorhus/linkify-…'), 'https://github.com/sindresorhus/linkify-… and https://github.com/sindresorhus/linkify-…');
t.is(linkifyUrls('https://github.com/sindresorhus/linkify-urls and more…'), '<a href="https://github.com/sindresorhus/linkify-urls">https://github.com/sindresorhus/linkify-urls</a> and more…');
t.is(linkifyUrlsToHtml('https://github.com/sindresorhus/linkify-…'), 'https://github.com/sindresorhus/linkify-…');
t.is(linkifyUrlsToHtml('https://github.com/sindresorhus/linkify-… and https://github.com/sindresorhus/linkify-…'), 'https://github.com/sindresorhus/linkify-… and https://github.com/sindresorhus/linkify-…');
t.is(linkifyUrlsToHtml('https://github.com/sindresorhus/linkify-urls and more…'), '<a href="https://github.com/sindresorhus/linkify-urls">https://github.com/sindresorhus/linkify-urls</a> and more…');

t.is(linkifyUrls('https://github.com/sindresorhus/linkify-...'), 'https://github.com/sindresorhus/linkify-...');
t.is(linkifyUrls('https://github.com/sindresorhus/linkify-... and https://github.com/sindresorhus/linkify-...'), 'https://github.com/sindresorhus/linkify-... and https://github.com/sindresorhus/linkify-...');
t.is(linkifyUrls('https://github.com/sindresorhus/linkify-urls and more...'), '<a href="https://github.com/sindresorhus/linkify-urls">https://github.com/sindresorhus/linkify-urls</a> and more...');
t.is(linkifyUrlsToHtml('https://github.com/sindresorhus/linkify-...'), 'https://github.com/sindresorhus/linkify-...');
t.is(linkifyUrlsToHtml('https://github.com/sindresorhus/linkify-... and https://github.com/sindresorhus/linkify-...'), 'https://github.com/sindresorhus/linkify-... and https://github.com/sindresorhus/linkify-...');
t.is(linkifyUrlsToHtml('https://github.com/sindresorhus/linkify-urls and more...'), '<a href="https://github.com/sindresorhus/linkify-urls">https://github.com/sindresorhus/linkify-urls</a> and more...');
});

test('skips truncated URLs (DocumentFragment)', t => {
t.is(
html(linkifyUrls('See https://github.com/sindresorhus/linkify-urls and https://github.com/sindresorhus/linkify-…', {
type: 'dom',
})),
html(linkifyUrlsToDom('See https://github.com/sindresorhus/linkify-urls and https://github.com/sindresorhus/linkify-…')),
html(domify('See <a href="https://github.com/sindresorhus/linkify-urls">https://github.com/sindresorhus/linkify-urls</a> and https://github.com/sindresorhus/linkify-…')),
);
t.is(
html(linkifyUrls('See https://github.com/sindresorhus/linkify-urls… and https://github.com/sindresorhus/linkify-…', {
type: 'dom',
})),
html(linkifyUrlsToDom('See https://github.com/sindresorhus/linkify-urls… and https://github.com/sindresorhus/linkify-…')),
html(domify('See https://github.com/sindresorhus/linkify-urls… and https://github.com/sindresorhus/linkify-…')),
);

t.is(
html(linkifyUrls('See https://github.com/sindresorhus/linkify-urls and https://github.com/sindresorhus/linkify-...', {
type: 'dom',
})),
html(linkifyUrlsToDom('See https://github.com/sindresorhus/linkify-urls and https://github.com/sindresorhus/linkify-...')),
html(domify('See <a href="https://github.com/sindresorhus/linkify-urls">https://github.com/sindresorhus/linkify-urls</a> and https://github.com/sindresorhus/linkify-...')),
);
t.is(
html(linkifyUrls('See https://github.com/sindresorhus/linkify-... and https://github.com/sindresorhus/linkify-...', {
type: 'dom',
})),
html(linkifyUrlsToDom('See https://github.com/sindresorhus/linkify-... and https://github.com/sindresorhus/linkify-...')),
html(domify('See https://github.com/sindresorhus/linkify-... and https://github.com/sindresorhus/linkify-...')),
);
});

0 comments on commit f679537

Please sign in to comment.