Skip to content

Commit 7471c52

Browse files
authored
linkify-html: don't convert & -> & (#462)
1 parent 3961e7b commit 7471c52

File tree

3 files changed

+111
-108
lines changed

3 files changed

+111
-108
lines changed

packages/linkify-html/package.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@
1717
"url": "git+https://github.com/Hypercontext/linkifyjs.git",
1818
"directory": "packages/linkify-html"
1919
},
20-
"keywords": [
21-
"link",
22-
"autolink",
23-
"url",
24-
"email"
25-
],
20+
"keywords": ["link", "autolink", "url", "email"],
2621
"author": "Hypercontext",
2722
"license": "MIT",
2823
"bugs": {

packages/linkify-html/src/linkify-html.js

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { tokenize as htmlTokenize } from '@nfrasser/simple-html-tokenizer';
2-
import { tokenize, Options} from 'linkifyjs';
2+
import { tokenize, Options } from 'linkifyjs';
33

44
const LinkifyResult = 'LinkifyResult';
55
const StartTag = 'StartTag';
@@ -32,7 +32,9 @@ export default function linkifyHtml(str, opts = {}) {
3232
// Ignore all the contents of ignored tags
3333
const tagName = token.tagName.toUpperCase();
3434
const isIgnored = tagName === 'A' || options.ignoreTags.indexOf(tagName) >= 0;
35-
if (!isIgnored) { continue; }
35+
if (!isIgnored) {
36+
continue;
37+
}
3638

3739
let preskipLen = linkifiedTokens.length;
3840
skipTagTokens(tagName, tokens, ++i, linkifiedTokens);
@@ -51,36 +53,42 @@ export default function linkifyHtml(str, opts = {}) {
5153
for (let i = 0; i < linkifiedTokens.length; i++) {
5254
const token = linkifiedTokens[i];
5355
switch (token.type) {
54-
case LinkifyResult:
55-
linkified.push(token.rendered);
56-
break;
57-
case StartTag: {
58-
let link = '<' + token.tagName;
59-
if (token.attributes.length > 0) {
60-
link += ' ' + attributeArrayToStrings(token.attributes).join(' ');
56+
case LinkifyResult:
57+
linkified.push(token.rendered);
58+
break;
59+
case StartTag: {
60+
let link = '<' + token.tagName;
61+
if (token.attributes.length > 0) {
62+
link += ' ' + attributeArrayToStrings(token.attributes).join(' ');
63+
}
64+
if (token.selfClosing) {
65+
link += ' /';
66+
}
67+
link += '>';
68+
linkified.push(link);
69+
break;
70+
}
71+
case EndTag:
72+
linkified.push(`</${token.tagName}>`);
73+
break;
74+
case Chars:
75+
linkified.push(escapeText(token.chars));
76+
break;
77+
case Comment:
78+
linkified.push(`<!--${escapeText(token.chars)}-->`);
79+
break;
80+
case Doctype: {
81+
let doctype = `<!DOCTYPE ${token.name}`;
82+
if (token.publicIdentifier) {
83+
doctype += ` PUBLIC "${token.publicIdentifier}"`;
84+
}
85+
if (token.systemIdentifier) {
86+
doctype += ` "${token.systemIdentifier}"`;
87+
}
88+
doctype += '>';
89+
linkified.push(doctype);
90+
break;
6191
}
62-
if (token.selfClosing) { link += ' /'; }
63-
link += '>';
64-
linkified.push(link);
65-
break;
66-
}
67-
case EndTag:
68-
linkified.push(`</${token.tagName}>`);
69-
break;
70-
case Chars:
71-
linkified.push(escapeText(token.chars));
72-
break;
73-
case Comment:
74-
linkified.push(`<!--${escapeText(token.chars)}-->`);
75-
break;
76-
case Doctype: {
77-
let doctype = `<!DOCTYPE ${token.name}`;
78-
if (token.publicIdentifier) { doctype += ` PUBLIC "${token.publicIdentifier}"`; }
79-
if (token.systemIdentifier) { doctype += ` "${token.systemIdentifier}"`; }
80-
doctype += '>';
81-
linkified.push(doctype);
82-
break;
83-
}
8492
}
8593
}
8694

@@ -104,14 +112,14 @@ function linkifyChars(str, options) {
104112
type: StartTag,
105113
tagName: 'br',
106114
attributes: [],
107-
selfClosing: true
115+
selfClosing: true,
108116
});
109117
} else if (!token.isLink || !options.check(token)) {
110118
result.push({ type: Chars, chars: token.toString() });
111119
} else {
112120
result.push({
113121
type: LinkifyResult,
114-
rendered: options.render(token)
122+
rendered: options.render(token),
115123
});
116124
}
117125
}
@@ -134,7 +142,6 @@ function linkifyChars(str, options) {
134142
* Will track whether there is a nested tag of the same type
135143
*/
136144
function skipTagTokens(tagName, tokens, i, skippedTokens) {
137-
138145
// number of tokens of this type on the [fictional] stack
139146
let stackCount = 1;
140147

@@ -162,10 +169,7 @@ function defaultRender({ tagName, attributes, content }) {
162169
}
163170

164171
function escapeText(text) {
165-
return text
166-
.replace(/&/g, '&amp;')
167-
.replace(/</g, '&lt;')
168-
.replace(/>/g, '&gt;');
172+
return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
169173
}
170174

171175
function escapeAttr(attr) {

test/spec/linkify-html.test.js

Lines changed: 68 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,69 +5,77 @@ const svg = [
55
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 801.197 614.273">',
66
'<rect height="304" width="554" y="50" x="50" stroke="#000" fill="#ff0000" />',
77
'<rect height="304" width="554" y="150" x="131" stroke="#000" fill="#fff" />',
8-
'</svg>'
8+
'</svg>',
99
].join('');
1010

1111
describe('linkify-html', () => {
12-
1312
// For each element in this array
1413
// [0] - Original text
1514
// [1] - Linkified with default options
1615
// [2] - Linkified with new options
1716
const tests = [
17+
['Test with no links', 'Test with no links', 'Test with no links'],
1818
[
19-
'Test with no links',
20-
'Test with no links',
21-
'Test with no links'
22-
], [
2319
'The URL is google.com and the email is <strong>[email protected]</strong><br>',
2420
'The URL is <a href="http://google.com">google.com</a> and the email is <strong><a href="mailto:[email protected]">[email protected]</a></strong><br>',
25-
'The URL is <span href="https://google.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">google.com</span> and the email is <strong><span href="mailto:[email protected]?subject=Hello%20from%20Linkify" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">[email protected]</span></strong><br>'
26-
], [
21+
'The URL is <span href="https://google.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">google.com</span> and the email is <strong><span href="mailto:[email protected]?subject=Hello%20from%20Linkify" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">[email protected]</span></strong><br>',
22+
],
23+
[
2724
'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: [email protected]!',
2825
'Super long maps URL <a href="https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en">https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en</a>, a #hash-tag, and an email: <a href="mailto:[email protected]">[email protected]</a>!',
2926
'Super long maps URL <span href="https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">https://www.google.ca/maps/@43.472082,-8…</span>, a #hash-tag, and an email: <span href="mailto:[email protected]?subject=Hello%20from%20Linkify" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">[email protected]</span>!',
30-
], [
27+
],
28+
[
3129
'This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt http://github.com</h1><br />',
3230
'This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt <a href="http://github.com">http://github.com</a></h1><br />',
33-
'This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt <span href="http://github.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">http://github.com</span></h1><br />'
34-
], [
31+
'This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt <span href="http://github.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">http://github.com</span></h1><br />',
32+
],
33+
[
34+
'Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/',
3535
'Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/',
3636
'Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/',
37-
'Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/'
38-
], [
37+
],
38+
[
3939
'Ignore tags like <script>const a = {}; a.ca = "Hello";</script> and <style>b.com {color: blue;}</style>',
4040
'Ignore tags like <script>const a = {}; <a href="http://a.ca">a.ca</a> = "Hello";</script> and <style><a href="http://b.com">b.com</a> {color: blue;}</style>',
41-
'Ignore tags like <script>const a = {}; a.ca = "Hello";</script> and <style>b.com {color: blue;}</style>'
42-
], [
41+
'Ignore tags like <script>const a = {}; a.ca = "Hello";</script> and <style>b.com {color: blue;}</style>',
42+
],
43+
[
4344
'Link followed by nbsp escape sequence https://github.com&nbsp;',
4445
'Link followed by nbsp escape sequence <a href="https://github.com">https://github.com</a>\u00a0',
45-
'Link followed by nbsp escape sequence <span href="https://github.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">https://github.com</span>\u00a0'
46-
], [
46+
'Link followed by nbsp escape sequence <span href="https://github.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">https://github.com</span>\u00a0',
47+
],
48+
[
4749
'Link surrounded by encoded quotes &quot;http://google.com&quot;',
4850
'Link surrounded by encoded quotes "<a href="http://google.com">http://google.com</a>"',
49-
'Link surrounded by encoded quotes "<span href="http://google.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">http://google.com</span>"'
50-
], [
51+
'Link surrounded by encoded quotes "<span href="http://google.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">http://google.com</span>"',
52+
],
53+
[
5154
'https:&#x2F;&#x2F;html5-chat.com&#x2F;',
5255
'<a href="https://html5-chat.com/">https://html5-chat.com/</a>',
53-
'<span href="https://html5-chat.com/" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">https://html5-chat.com/</span>'
54-
], [
56+
'<span href="https://html5-chat.com/" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">https://html5-chat.com/</span>',
57+
],
58+
[
5559
'Surrounded by lt/gt symbols &lt;http://nu.nl&gt;',
5660
'Surrounded by lt/gt symbols &lt;<a href="http://nu.nl">http://nu.nl</a>&gt;',
57-
'Surrounded by lt/gt symbols &lt;<span href="http://nu.nl" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">http://nu.nl</span>&gt;'
58-
], [
61+
'Surrounded by lt/gt symbols &lt;<span href="http://nu.nl" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">http://nu.nl</span>&gt;',
62+
],
63+
[
5964
'http://xml.example.com/pub.dtd?a=1&b=2',
60-
'<a href="http://xml.example.com/pub.dtd?a=1&b=2">http://xml.example.com/pub.dtd?a=1&amp;b=2</a>',
61-
'<span href="http://xml.example.com/pub.dtd?a=1&b=2" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">http://xml.example.com/pub.dtd?a=1&amp;b=2</span>'
62-
], [
63-
svg,
64-
svg,
65-
svg
66-
], [
65+
'<a href="http://xml.example.com/pub.dtd?a=1&b=2">http://xml.example.com/pub.dtd?a=1&b=2</a>',
66+
'<span href="http://xml.example.com/pub.dtd?a=1&b=2" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">http://xml.example.com/pub.dtd?a=1&b=2</span>',
67+
],
68+
[svg, svg, svg],
69+
[
6770
'Does nl2br.com work?\nYes',
6871
'Does <a href="http://nl2br.com">nl2br.com</a> work?\nYes',
6972
'Does <span href="https://nl2br.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">nl2br.com</span> work?<br />Yes',
70-
]
73+
],
74+
[
75+
'<p>Here is a link and an extra space: google.com &nbsp;</p><p>Here is a link and a greater-than: google.com &gt;</p><p>Here is a link and an ellipsis: google.com &hellip;</p>',
76+
'<p>Here is a link and an extra space: <a href="http://google.com">google.com</a> \u00a0</p><p>Here is a link and a greater-than: <a href="http://google.com">google.com</a> &gt;</p><p>Here is a link and an ellipsis: <a href="http://google.com">google.com</a> &hellip;</p>',
77+
'<p>Here is a link and an extra space: <span href="https://google.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">google.com</span> \u00a0</p><p>Here is a link and a greater-than: <span href="https://google.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">google.com</span> &gt;</p><p>Here is a link and an ellipsis: <span href="https://google.com" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">google.com</span> &hellip;</p>',
78+
],
7179
];
7280

7381
let options;
@@ -80,18 +88,15 @@ describe('linkify-html', () => {
8088
defaultProtocol: 'https',
8189
rel: 'nofollow',
8290
attributes: {
83-
onclick: 'console.log(\'Hello World!\')'
91+
onclick: "console.log('Hello World!')",
8492
},
8593
format(val) {
8694
return val.truncate(40);
8795
},
8896
formatHref: {
89-
email: (href) => href + '?subject=Hello%20from%20Linkify'
97+
email: (href) => href + '?subject=Hello%20from%20Linkify',
9098
},
91-
ignoreTags: [
92-
'script',
93-
'style'
94-
]
99+
ignoreTags: ['script', 'style'],
95100
};
96101
});
97102

@@ -110,43 +115,44 @@ describe('linkify-html', () => {
110115
it('Works with truncate options (truncate has priority in formatting chars)', () => {
111116
options.truncate = 30;
112117

113-
expect(linkifyHtml(
114-
'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en',
115-
options
116-
)).to.be.eql(
117-
'Super long maps URL <span href="https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">https://www.google.ca/maps/@43…</span>'
118+
expect(
119+
linkifyHtml('Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en', options),
120+
).to.be.eql(
121+
'Super long maps URL <span href="https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">https://www.google.ca/maps/@43…</span>',
118122
);
119123
});
120124

121125
it('Works with overriden options (validate)', () => {
122126
const optionsValidate = {
123127
validate: {
124128
url: function (text) {
125-
return /^(http|ftp)s?:\/\//.test(text) || text.slice(0,3) === 'www';
126-
}
127-
}
129+
return /^(http|ftp)s?:\/\//.test(text) || text.slice(0, 3) === 'www';
130+
},
131+
},
128132
};
129133

130134
const testsValidate = [
135+
['1.Test with no links', '1.Test with no links'],
131136
[
132-
'1.Test with no links',
133-
'1.Test with no links'
134-
], [
135137
'2.The URL is google.com and the email is <strong>[email protected]</strong>',
136-
'2.The URL is google.com and the email is <strong><a href="mailto:[email protected]">[email protected]</a></strong>'
137-
], [
138+
'2.The URL is google.com and the email is <strong><a href="mailto:[email protected]">[email protected]</a></strong>',
139+
],
140+
[
138141
'3.Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: [email protected]!',
139-
'3.Super long maps URL <a href="https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en">https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en</a>, a #hash-tag, and an email: <a href="mailto:[email protected]">[email protected]</a>!'
140-
], [
142+
'3.Super long maps URL <a href="https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en">https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en</a>, a #hash-tag, and an email: <a href="mailto:[email protected]">[email protected]</a>!',
143+
],
144+
[
141145
'4a.This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt http://github.com</h1>',
142-
'4a.This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt <a href="http://github.com">http://github.com</a></h1>'
143-
], [
146+
'4a.This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt <a href="http://github.com">http://github.com</a></h1>',
147+
],
148+
[
149+
'4b.This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt github.com</h1>',
144150
'4b.This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt github.com</h1>',
145-
'4b.This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt github.com</h1>'
146-
], [
151+
],
152+
[
147153
'5.Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/',
148-
'5.Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/'
149-
]
154+
'5.Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/',
155+
],
150156
];
151157

152158
testsValidate.map(function (test) {
@@ -160,15 +166,12 @@ describe('linkify-html', () => {
160166
});
161167

162168
it('Works with HTML and overriden options', () => {
163-
const linkified = linkifyHtml(
164-
htmlOptions.original,
165-
htmlOptions.altOptions
166-
);
169+
const linkified = linkifyHtml(htmlOptions.original, htmlOptions.altOptions);
167170
expect(linkified).to.be.oneOf(htmlOptions.linkifiedAlt);
168171
});
169172

170173
it('Treats null target options properly', () => {
171-
let linkified = linkifyHtml('http://google.com', { target: { url: null }});
174+
let linkified = linkifyHtml('http://google.com', { target: { url: null } });
172175
expect(linkified).to.be.eql('<a href="http://google.com">http://google.com</a>');
173176

174177
linkified = linkifyHtml('http://google.com', { target: null });
@@ -196,7 +199,8 @@ describe('linkify-html', () => {
196199
});
197200

198201
it('Handles mixed-language content', () => {
199-
const input = '這禮拜是我們新的循環 (3/23-4/19), 我將於這週日給 Jeffrey 補課,並且我們會在這期間選另外一個可以上課的日期。';
202+
const input =
203+
'這禮拜是我們新的循環 (3/23-4/19), 我將於這週日給 Jeffrey 補課,並且我們會在這期間選另外一個可以上課的日期。';
200204
expect(linkifyHtml(input)).to.be.ok;
201205
});
202206

0 commit comments

Comments
 (0)