Skip to content

Commit aa737dc

Browse files
author
Nick Frasser
authored
Add explicit support for mailto: addresses (#186)
* Add explicit support for mailto: addresses * Tests for mailto: links * parser.js indentation fix
1 parent 2f512ea commit aa737dc

File tree

8 files changed

+132
-36
lines changed

8 files changed

+132
-36
lines changed

src/linkify/core/parser.js

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
PLUS,
2929
POUND,
3030
PROTOCOL,
31+
MAILTO,
3132
QUERY,
3233
SLASH,
3334
UNDERSCORE,
@@ -45,6 +46,7 @@ import {
4546
} from './tokens/text';
4647

4748
import {
49+
MAILTOEMAIL,
4850
EMAIL,
4951
NL as MNL,
5052
TEXT,
@@ -58,42 +60,46 @@ let S_START = makeState();
5860

5961
// Intermediate states for URLs. Note that domains that begin with a protocol
6062
// are treated slighly differently from those that don't.
61-
let S_PROTOCOL = makeState(); // e.g., 'http:'
62-
let S_PROTOCOL_SLASH = makeState(); // e.g., '/', 'http:/''
63-
let S_PROTOCOL_SLASH_SLASH = makeState(); // e.g., '//', 'http://'
64-
let S_DOMAIN = makeState(); // parsed string ends with a potential domain name (A)
65-
let S_DOMAIN_DOT = makeState(); // (A) domain followed by DOT
66-
let S_TLD = makeState(URL); // (A) Simplest possible URL with no query string
67-
let S_TLD_COLON = makeState(); // (A) URL followed by colon (potential port number here)
68-
let S_TLD_PORT = makeState(URL); // TLD followed by a port number
69-
let S_URL = makeState(URL); // Long URL with optional port and maybe query string
70-
let S_URL_NON_ACCEPTING = makeState(); // URL followed by some symbols (will not be part of the final URL)
71-
let S_URL_OPENBRACE = makeState(); // URL followed by {
72-
let S_URL_OPENBRACKET = makeState(); // URL followed by [
73-
let S_URL_OPENANGLEBRACKET = makeState(); // URL followed by <
74-
let S_URL_OPENPAREN = makeState(); // URL followed by (
75-
let S_URL_OPENBRACE_Q = makeState(URL); // URL followed by { and some symbols that the URL can end it
76-
let S_URL_OPENBRACKET_Q = makeState(URL); // URL followed by [ and some symbols that the URL can end it
77-
let S_URL_OPENANGLEBRACKET_Q = makeState(URL); // URL followed by < and some symbols that the URL can end it
78-
let S_URL_OPENPAREN_Q = makeState(URL); // URL followed by ( and some symbols that the URL can end it
79-
let S_URL_OPENBRACE_SYMS = makeState(); // S_URL_OPENBRACE_Q followed by some symbols it cannot end it
80-
let S_URL_OPENBRACKET_SYMS = makeState(); // S_URL_OPENBRACKET_Q followed by some symbols it cannot end it
81-
let S_URL_OPENANGLEBRACKET_SYMS = makeState(); // S_URL_OPENANGLEBRACKET_Q followed by some symbols it cannot end it
82-
let S_URL_OPENPAREN_SYMS = makeState(); // S_URL_OPENPAREN_Q followed by some symbols it cannot end it
83-
let S_EMAIL_DOMAIN = makeState(); // parsed string starts with local email info + @ with a potential domain name (C)
84-
let S_EMAIL_DOMAIN_DOT = makeState(); // (C) domain followed by DOT
85-
let S_EMAIL = makeState(EMAIL); // (C) Possible email address (could have more tlds)
86-
let S_EMAIL_COLON = makeState(); // (C) URL followed by colon (potential port number here)
87-
let S_EMAIL_PORT = makeState(EMAIL); // (C) Email address with a port
88-
let S_LOCALPART = makeState(); // Local part of the email address
89-
let S_LOCALPART_AT = makeState(); // Local part of the email address plus @
90-
let S_LOCALPART_DOT = makeState(); // Local part of the email address plus '.' (localpart cannot end in .)
91-
let S_NL = makeState(MNL); // single new line
63+
let S_PROTOCOL = makeState(); // e.g., 'http:'
64+
let S_MAILTO = makeState(); // 'mailto:'
65+
let S_PROTOCOL_SLASH = makeState(); // e.g., '/', 'http:/''
66+
let S_PROTOCOL_SLASH_SLASH = makeState(); // e.g., '//', 'http://'
67+
let S_DOMAIN = makeState(); // parsed string ends with a potential domain name (A)
68+
let S_DOMAIN_DOT = makeState(); // (A) domain followed by DOT
69+
let S_TLD = makeState(URL); // (A) Simplest possible URL with no query string
70+
let S_TLD_COLON = makeState(); // (A) URL followed by colon (potential port number here)
71+
let S_TLD_PORT = makeState(URL); // TLD followed by a port number
72+
let S_URL = makeState(URL); // Long URL with optional port and maybe query string
73+
let S_URL_NON_ACCEPTING = makeState(); // URL followed by some symbols (will not be part of the final URL)
74+
let S_URL_OPENBRACE = makeState(); // URL followed by {
75+
let S_URL_OPENBRACKET = makeState(); // URL followed by [
76+
let S_URL_OPENANGLEBRACKET = makeState(); // URL followed by <
77+
let S_URL_OPENPAREN = makeState(); // URL followed by (
78+
let S_URL_OPENBRACE_Q = makeState(URL); // URL followed by { and some symbols that the URL can end it
79+
let S_URL_OPENBRACKET_Q = makeState(URL); // URL followed by [ and some symbols that the URL can end it
80+
let S_URL_OPENANGLEBRACKET_Q = makeState(URL); // URL followed by < and some symbols that the URL can end it
81+
let S_URL_OPENPAREN_Q = makeState(URL); // URL followed by ( and some symbols that the URL can end it
82+
let S_URL_OPENBRACE_SYMS = makeState(); // S_URL_OPENBRACE_Q followed by some symbols it cannot end it
83+
let S_URL_OPENBRACKET_SYMS = makeState(); // S_URL_OPENBRACKET_Q followed by some symbols it cannot end it
84+
let S_URL_OPENANGLEBRACKET_SYMS = makeState(); // S_URL_OPENANGLEBRACKET_Q followed by some symbols it cannot end it
85+
let S_URL_OPENPAREN_SYMS = makeState(); // S_URL_OPENPAREN_Q followed by some symbols it cannot end it
86+
let S_EMAIL_DOMAIN = makeState(); // parsed string starts with local email info + @ with a potential domain name (C)
87+
let S_EMAIL_DOMAIN_DOT = makeState(); // (C) domain followed by DOT
88+
let S_EMAIL = makeState(EMAIL); // (C) Possible email address (could have more tlds)
89+
let S_EMAIL_COLON = makeState(); // (C) URL followed by colon (potential port number here)
90+
let S_EMAIL_PORT = makeState(EMAIL); // (C) Email address with a port
91+
let S_MAILTO_EMAIL = makeState(MAILTOEMAIL); // Email that begins with the mailto prefix (D)
92+
let S_MAILTO_EMAIL_NON_ACCEPTING = makeState(); // (D) Followed by some non-query string chars
93+
let S_LOCALPART = makeState(); // Local part of the email address
94+
let S_LOCALPART_AT = makeState(); // Local part of the email address plus @
95+
let S_LOCALPART_DOT = makeState(); // Local part of the email address plus '.' (localpart cannot end in .)
96+
let S_NL = makeState(MNL); // single new line
9297

9398
// Make path from start to protocol (with '//')
9499
S_START
95100
.on(TNL, S_NL)
96101
.on(PROTOCOL, S_PROTOCOL)
102+
.on(MAILTO, S_MAILTO)
97103
.on(SLASH, S_PROTOCOL_SLASH);
98104

99105
S_PROTOCOL.on(SLASH, S_PROTOCOL_SLASH);
@@ -255,6 +261,23 @@ S_URL_NON_ACCEPTING.on(qsNonAccepting, S_URL_NON_ACCEPTING);
255261
// Note: We are not allowing '/' in email addresses since this would interfere
256262
// with real URLs
257263

264+
// For addresses with the mailto prefix
265+
// 'mailto:' followed by anything sane is a valid email
266+
S_MAILTO
267+
.on(TLD, S_MAILTO_EMAIL)
268+
.on(DOMAIN, S_MAILTO_EMAIL)
269+
.on(NUM, S_MAILTO_EMAIL)
270+
.on(LOCALHOST, S_MAILTO_EMAIL);
271+
272+
// Greedily get more potential valid email values
273+
S_MAILTO_EMAIL
274+
.on(qsAccepting, S_MAILTO_EMAIL)
275+
.on(qsNonAccepting, S_MAILTO_EMAIL_NON_ACCEPTING);
276+
S_MAILTO_EMAIL_NON_ACCEPTING
277+
.on(qsAccepting, S_MAILTO_EMAIL)
278+
.on(qsNonAccepting, S_MAILTO_EMAIL_NON_ACCEPTING);
279+
280+
// For addresses without the mailto prefix
258281
// Tokens allowed in the localpart of the email
259282
let localpartAccepting = [
260283
DOMAIN,

src/linkify/core/scanner.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
LOCALHOST,
1515
NUM,
1616
PROTOCOL,
17+
MAILTO,
1718
TLD,
1819
WS,
1920
AT,
@@ -95,6 +96,7 @@ for (let i = 0; i < tlds.length; i++) {
9596
let partialProtocolFileStates = stateify('file', S_START, DOMAIN, DOMAIN);
9697
let partialProtocolFtpStates = stateify('ftp', S_START, DOMAIN, DOMAIN);
9798
let partialProtocolHttpStates = stateify('http', S_START, DOMAIN, DOMAIN);
99+
let partialProtocolMailtoStates = stateify('mailto', S_START, DOMAIN, DOMAIN);
98100

99101
// Add the states to the array of DOMAINeric states
100102
domainStates.push.apply(domainStates, partialProtocolFileStates);
@@ -105,8 +107,10 @@ domainStates.push.apply(domainStates, partialProtocolHttpStates);
105107
let S_PROTOCOL_FILE = partialProtocolFileStates.pop();
106108
let S_PROTOCOL_FTP = partialProtocolFtpStates.pop();
107109
let S_PROTOCOL_HTTP = partialProtocolHttpStates.pop();
110+
let S_MAILTO = partialProtocolMailtoStates.pop();
108111
let S_PROTOCOL_SECURE = makeState(DOMAIN);
109112
let S_FULL_PROTOCOL = makeState(PROTOCOL); // Full protocol ends with COLON
113+
let S_FULL_MAILTO = makeState(MAILTO); // Mailto ends with COLON
110114

111115
// Secure protocols (end with 's')
112116
S_PROTOCOL_FTP
@@ -122,6 +126,7 @@ domainStates.push(S_PROTOCOL_SECURE);
122126
// Become protocol tokens after a COLON
123127
S_PROTOCOL_FILE.on(':', S_FULL_PROTOCOL);
124128
S_PROTOCOL_SECURE.on(':', S_FULL_PROTOCOL);
129+
S_MAILTO.on(':', S_FULL_MAILTO);
125130

126131
// Localhost
127132
let partialLocalhostStates = stateify('localhost', S_START, LOCALHOST, DOMAIN);

src/linkify/core/tokens/multi.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {createTokenClass} from './create-token-class';
22
import {inherits} from '../../utils/class';
3-
import {DOMAIN, PROTOCOL, TLD, SLASH} from './text';
3+
import {DOMAIN, PROTOCOL, TLD, SLASH, MAILTO} from './text';
44

55
/******************************************************************************
66
Multi-Tokens
@@ -85,6 +85,16 @@ MultiToken.prototype = {
8585
}
8686
};
8787

88+
/**
89+
Represents an arbitrarily mailto email address with the prefix included
90+
@class MAILTO
91+
@extends MultiToken
92+
*/
93+
const MAILTOEMAIL = inherits(MultiToken, createTokenClass(), {
94+
type: 'email',
95+
isLink: true
96+
});
97+
8898
/**
8999
Represents a list of tokens making up a valid email address
90100
@class EMAIL
@@ -94,6 +104,7 @@ const EMAIL = inherits(MultiToken, createTokenClass(), {
94104
type: 'email',
95105
isLink: true,
96106
toHref() {
107+
let tokens = this.v;
97108
return 'mailto:' + this.toString();
98109
}
99110
});
@@ -179,6 +190,7 @@ const URL = inherits(MultiToken, createTokenClass(), {
179190

180191
export {
181192
MultiToken as Base,
193+
MAILTOEMAIL,
182194
EMAIL,
183195
NL,
184196
TEXT,

src/linkify/core/tokens/text.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,20 @@ const POUND = inheritsToken('#');
101101
* `https:`
102102
* `ftp:`
103103
* `ftps:`
104-
* There's Another super weird one
105104
106105
@class PROTOCOL
107106
@extends TextToken
108107
*/
109108
const PROTOCOL = inheritsToken();
110109

110+
/**
111+
Represents the start of the email URI protocol
112+
113+
@class MAILTO
114+
@extends TextToken
115+
*/
116+
const MAILTO = inheritsToken('mailto:');
117+
111118
/**
112119
@class QUERY
113120
@extends TextToken
@@ -176,6 +183,7 @@ export {
176183
POUND,
177184
QUERY,
178185
PROTOCOL,
186+
MAILTO,
179187
SLASH,
180188
UNDERSCORE,
181189
SYM,

test/spec/linkify-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ describe('linkify', () => {
5454
['[email protected]', true],
5555
['[email protected]', false, 'url'],
5656
['[email protected]', true, 'email'],
57+
['mailto:[email protected]', true, 'email'],
5758
['t.co', true],
5859
['t.co g.co', false], // can only be one
5960
['[email protected] t.co', false] // can only be one

test/spec/linkify/core/parser-test.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const MULTI_TOKENS = require(`${__base}linkify/core/tokens`).multi;
55
const TEXT = MULTI_TOKENS.TEXT;
66
const URL = MULTI_TOKENS.URL;
77
const EMAIL = MULTI_TOKENS.EMAIL;
8+
const MAILTOEMAIL = MULTI_TOKENS.MAILTOEMAIL;
89

910
/**
1011
[0] - Original text to parse (should tokenize first)
@@ -108,9 +109,17 @@ var tests = [
108109
[TEXT, EMAIL],
109110
['Emails cannot have two dots, e.g.: nick..', '[email protected]']
110111
], [
111-
'The `mailto:` part should not be included in mailto:[email protected]',
112-
[TEXT, EMAIL],
113-
['The `mailto:` part should not be included in mailto:', '[email protected]']
112+
'The `mailto:` part should be included in mailto:[email protected]',
113+
[TEXT, MAILTOEMAIL],
114+
['The `mailto:` part should be included in ', 'mailto:[email protected]']
115+
], [
116+
'mailto:[email protected]?Subject=Hello%20again is another test',
117+
[MAILTOEMAIL, TEXT],
118+
['mailto:[email protected]?Subject=Hello%20again', ' is another test']
119+
], [
120+
'Mailto is greedy mailto:localhost?subject=Hello%20World.',
121+
[TEXT, MAILTOEMAIL, TEXT],
122+
['Mailto is greedy ', 'mailto:localhost?subject=Hello%20World', '.']
114123
], [
115124
'Bu haritanın verileri Direniş İzleme Grubu\'nun yaptığı Türkiye İşçi Eylemleri haritası ile birleşebilir esasen. https://graphcommons.com/graphs/00af1cd8-5a67-40b1-86e5-32beae436f7c?show=Comments',
116125
[TEXT, URL],

test/spec/linkify/core/scanner-test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const NUM = TEXTTOKENS.NUM;
1212
const PLUS = TEXTTOKENS.PLUS;
1313
const POUND = TEXTTOKENS.POUND;
1414
const PROTOCOL = TEXTTOKENS.PROTOCOL;
15+
const MAILTO = TEXTTOKENS.MAILTO;
1516
const QUERY = TEXTTOKENS.QUERY;
1617
const SLASH = TEXTTOKENS.SLASH;
1718
const SYM = TEXTTOKENS.SYM;
@@ -56,6 +57,8 @@ const tests = [
5657
['files:', [DOMAIN, COLON], ['files', ':']],
5758
['file//', [DOMAIN, SLASH, SLASH], ['file', '/', '/']],
5859
['ftp://', [PROTOCOL, SLASH, SLASH], ['ftp:', '/', '/']],
60+
['mailto', [DOMAIN], ['mailto']],
61+
['mailto:', [MAILTO], ['mailto:']],
5962
['c', [DOMAIN], ['c']],
6063
['co', [TLD], ['co']],
6164
['com', [TLD], ['com']],

test/spec/linkify/core/tokens/multi-test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,41 @@ describe('linkify/core/tokens/MULTI_TOKENS', () => {
156156

157157
});
158158

159+
describe('MAILTOEMAIL', () => {
160+
var emailTextTokens, email;
161+
162+
before(() => {
163+
emailTextTokens = [ // test@example.com
164+
new TEXT_TOKENS.MAILTO(),
165+
new TEXT_TOKENS.DOMAIN('test'),
166+
new TEXT_TOKENS.AT(),
167+
new TEXT_TOKENS.DOMAIN('example'),
168+
new TEXT_TOKENS.DOT(),
169+
new TEXT_TOKENS.TLD('com')
170+
];
171+
email = new MULTI_TOKENS.MAILTOEMAIL(emailTextTokens);
172+
});
173+
174+
describe('#isLink', () => {
175+
it('Is true in all cases', () => {
176+
expect(email.isLink).to.be.ok;
177+
});
178+
});
179+
180+
describe('#toString()', () => {
181+
it('Returns mailto:[email protected]', () => {
182+
expect(email.toString()).to.be.eql('mailto:[email protected]');
183+
});
184+
});
185+
186+
describe('#toHref()', () => {
187+
it('Returns mailto:[email protected]', () => {
188+
expect(email.toHref()).to.be.eql('mailto:[email protected]');
189+
});
190+
});
191+
192+
});
193+
159194
describe('NL', () => {
160195
var nlTokens, nl;
161196

0 commit comments

Comments
 (0)