Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

linkify-html: don't convert & -> & #462

Merged
merged 1 commit into from
Nov 22, 2023
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
7 changes: 1 addition & 6 deletions packages/linkify-html/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
"url": "git+https://github.com/Hypercontext/linkifyjs.git",
"directory": "packages/linkify-html"
},
"keywords": [
"link",
"autolink",
"url",
"email"
],
"keywords": ["link", "autolink", "url", "email"],
"author": "Hypercontext",
"license": "MIT",
"bugs": {
Expand Down
80 changes: 42 additions & 38 deletions packages/linkify-html/src/linkify-html.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tokenize as htmlTokenize } from '@nfrasser/simple-html-tokenizer';
import { tokenize, Options} from 'linkifyjs';
import { tokenize, Options } from 'linkifyjs';

const LinkifyResult = 'LinkifyResult';
const StartTag = 'StartTag';
Expand Down Expand Up @@ -32,7 +32,9 @@ export default function linkifyHtml(str, opts = {}) {
// Ignore all the contents of ignored tags
const tagName = token.tagName.toUpperCase();
const isIgnored = tagName === 'A' || options.ignoreTags.indexOf(tagName) >= 0;
if (!isIgnored) { continue; }
if (!isIgnored) {
continue;
}

let preskipLen = linkifiedTokens.length;
skipTagTokens(tagName, tokens, ++i, linkifiedTokens);
Expand All @@ -51,36 +53,42 @@ export default function linkifyHtml(str, opts = {}) {
for (let i = 0; i < linkifiedTokens.length; i++) {
const token = linkifiedTokens[i];
switch (token.type) {
case LinkifyResult:
linkified.push(token.rendered);
break;
case StartTag: {
let link = '<' + token.tagName;
if (token.attributes.length > 0) {
link += ' ' + attributeArrayToStrings(token.attributes).join(' ');
case LinkifyResult:
linkified.push(token.rendered);
break;
case StartTag: {
let link = '<' + token.tagName;
if (token.attributes.length > 0) {
link += ' ' + attributeArrayToStrings(token.attributes).join(' ');
}
if (token.selfClosing) {
link += ' /';
}
link += '>';
linkified.push(link);
break;
}
case EndTag:
linkified.push(`</${token.tagName}>`);
break;
case Chars:
linkified.push(escapeText(token.chars));
break;
case Comment:
linkified.push(`<!--${escapeText(token.chars)}-->`);
break;
case Doctype: {
let doctype = `<!DOCTYPE ${token.name}`;
if (token.publicIdentifier) {
doctype += ` PUBLIC "${token.publicIdentifier}"`;
}
if (token.systemIdentifier) {
doctype += ` "${token.systemIdentifier}"`;
}
doctype += '>';
linkified.push(doctype);
break;
}
if (token.selfClosing) { link += ' /'; }
link += '>';
linkified.push(link);
break;
}
case EndTag:
linkified.push(`</${token.tagName}>`);
break;
case Chars:
linkified.push(escapeText(token.chars));
break;
case Comment:
linkified.push(`<!--${escapeText(token.chars)}-->`);
break;
case Doctype: {
let doctype = `<!DOCTYPE ${token.name}`;
if (token.publicIdentifier) { doctype += ` PUBLIC "${token.publicIdentifier}"`; }
if (token.systemIdentifier) { doctype += ` "${token.systemIdentifier}"`; }
doctype += '>';
linkified.push(doctype);
break;
}
}
}

Expand All @@ -104,14 +112,14 @@ function linkifyChars(str, options) {
type: StartTag,
tagName: 'br',
attributes: [],
selfClosing: true
selfClosing: true,
});
} else if (!token.isLink || !options.check(token)) {
result.push({ type: Chars, chars: token.toString() });
} else {
result.push({
type: LinkifyResult,
rendered: options.render(token)
rendered: options.render(token),
});
}
}
Expand All @@ -134,7 +142,6 @@ function linkifyChars(str, options) {
* Will track whether there is a nested tag of the same type
*/
function skipTagTokens(tagName, tokens, i, skippedTokens) {

// number of tokens of this type on the [fictional] stack
let stackCount = 1;

Expand Down Expand Up @@ -162,10 +169,7 @@ function defaultRender({ tagName, attributes, content }) {
}

function escapeText(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function escapeAttr(attr) {
Expand Down
132 changes: 68 additions & 64 deletions test/spec/linkify-html.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,69 +5,77 @@ const svg = [
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 801.197 614.273">',
'<rect height="304" width="554" y="50" x="50" stroke="#000" fill="#ff0000" />',
'<rect height="304" width="554" y="150" x="131" stroke="#000" fill="#fff" />',
'</svg>'
'</svg>',
].join('');

describe('linkify-html', () => {

// For each element in this array
// [0] - Original text
// [1] - Linkified with default options
// [2] - Linkified with new options
const tests = [
['Test with no links', 'Test with no links', 'Test with no links'],
[
'Test with no links',
'Test with no links',
'Test with no links'
], [
'The URL is google.com and the email is <strong>test@example.com</strong><br>',
'The URL is <a href="http://google.com">google.com</a> and the email is <strong><a href="mailto:test@example.com">test@example.com</a></strong><br>',
'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:test@example.com?subject=Hello%20from%20Linkify" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">test@example.com</span></strong><br>'
], [
'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:test@example.com?subject=Hello%20from%20Linkify" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">test@example.com</span></strong><br>',
],
[
'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: test.wut.yo@gmail.co.uk!',
'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:test.wut.yo@gmail.co.uk">test.wut.yo@gmail.co.uk</a>!',
'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:test.wut.yo@gmail.co.uk?subject=Hello%20from%20Linkify" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">test.wut.yo@gmail.co.uk</span>!',
], [
],
[
'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 />',
'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 />',
'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 />'
], [
'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 />',
],
[
'Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/',
'Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/',
'Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/',
'Unterminated anchor tag <a href="http://google.com"> This <em>is a link google.com</em> and this works!! https://reddit.com/r/photography/'
], [
],
[
'Ignore tags like <script>const a = {}; a.ca = "Hello";</script> and <style>b.com {color: blue;}</style>',
'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>',
'Ignore tags like <script>const a = {}; a.ca = "Hello";</script> and <style>b.com {color: blue;}</style>'
], [
'Ignore tags like <script>const a = {}; a.ca = "Hello";</script> and <style>b.com {color: blue;}</style>',
],
[
'Link followed by nbsp escape sequence https://github.com&nbsp;',
'Link followed by nbsp escape sequence <a href="https://github.com">https://github.com</a>\u00a0',
'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'
], [
'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',
],
[
'Link surrounded by encoded quotes &quot;http://google.com&quot;',
'Link surrounded by encoded quotes "<a href="http://google.com">http://google.com</a>"',
'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>"'
], [
'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>"',
],
[
'https:&#x2F;&#x2F;html5-chat.com&#x2F;',
'<a href="https://html5-chat.com/">https://html5-chat.com/</a>',
'<span href="https://html5-chat.com/" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">https://html5-chat.com/</span>'
], [
'<span href="https://html5-chat.com/" class="my-linkify-class" target="_parent" rel="nofollow" onclick="console.log(\'Hello World!\')">https://html5-chat.com/</span>',
],
[
'Surrounded by lt/gt symbols &lt;http://nu.nl&gt;',
'Surrounded by lt/gt symbols &lt;<a href="http://nu.nl">http://nu.nl</a>&gt;',
'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;'
], [
'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;',
],
[
'http://xml.example.com/pub.dtd?a=1&b=2',
'<a href="http://xml.example.com/pub.dtd?a=1&b=2">http://xml.example.com/pub.dtd?a=1&amp;b=2</a>',
'<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>'
], [
svg,
svg,
svg
], [
'<a href="http://xml.example.com/pub.dtd?a=1&b=2">http://xml.example.com/pub.dtd?a=1&b=2</a>',
'<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>',
],
[svg, svg, svg],
[
'Does nl2br.com work?\nYes',
'Does <a href="http://nl2br.com">nl2br.com</a> work?\nYes',
'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',
]
],
[
'<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>',
'<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>',
'<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>',
],
];

let options;
Expand All @@ -80,18 +88,15 @@ describe('linkify-html', () => {
defaultProtocol: 'https',
rel: 'nofollow',
attributes: {
onclick: 'console.log(\'Hello World!\')'
onclick: "console.log('Hello World!')",
},
format(val) {
return val.truncate(40);
},
formatHref: {
email: (href) => href + '?subject=Hello%20from%20Linkify'
email: (href) => href + '?subject=Hello%20from%20Linkify',
},
ignoreTags: [
'script',
'style'
]
ignoreTags: ['script', 'style'],
};
});

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

expect(linkifyHtml(
'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en',
options
)).to.be.eql(
'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>'
expect(
linkifyHtml('Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en', options),
).to.be.eql(
'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>',
);
});

it('Works with overriden options (validate)', () => {
const optionsValidate = {
validate: {
url: function (text) {
return /^(http|ftp)s?:\/\//.test(text) || text.slice(0,3) === 'www';
}
}
return /^(http|ftp)s?:\/\//.test(text) || text.slice(0, 3) === 'www';
},
},
};

const testsValidate = [
['1.Test with no links', '1.Test with no links'],
[
'1.Test with no links',
'1.Test with no links'
], [
'2.The URL is google.com and the email is <strong>test@example.com</strong>',
'2.The URL is google.com and the email is <strong><a href="mailto:test@example.com">test@example.com</a></strong>'
], [
'2.The URL is google.com and the email is <strong><a href="mailto:test@example.com">test@example.com</a></strong>',
],
[
'3.Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: test.wut.yo@gmail.co.uk!',
'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:test.wut.yo@gmail.co.uk">test.wut.yo@gmail.co.uk</a>!'
], [
'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:test.wut.yo@gmail.co.uk">test.wut.yo@gmail.co.uk</a>!',
],
[
'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>',
'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>'
], [
'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>',
],
[
'4b.This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt github.com</h1>',
'4b.This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt github.com</h1>',
'4b.This link is already in an anchor tag <a href="#bro">google.com</a> LOL and this one <h1>isnt github.com</h1>'
], [
],
[
'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/',
'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/'
]
'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/',
],
];

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

it('Works with HTML and overriden options', () => {
const linkified = linkifyHtml(
htmlOptions.original,
htmlOptions.altOptions
);
const linkified = linkifyHtml(htmlOptions.original, htmlOptions.altOptions);
expect(linkified).to.be.oneOf(htmlOptions.linkifiedAlt);
});

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

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

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

Expand Down