Skip to content

Commit

Permalink
Ensure linkify.find respects validate option (#458)
Browse files Browse the repository at this point in the history
  • Loading branch information
nfrasser authored Nov 9, 2023
1 parent fa65440 commit 17406b8
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 61 deletions.
32 changes: 20 additions & 12 deletions packages/linkifyjs/src/linkify.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { init as initParser, run as runParser } from './parser';
import { Options } from './options';
import { State } from './fsm';

const warn = typeof console !== 'undefined' && console && console.warn || (() => {});
const warnAdvice = 'until manual call of linkify.init(). Register all schemes and plugins before invoking linkify the first time.';
const warn = (typeof console !== 'undefined' && console && console.warn) || (() => {});
const warnAdvice =
'until manual call of linkify.init(). Register all schemes and plugins before invoking linkify the first time.';

// Side-effect initialization state
const INIT = {
Expand Down Expand Up @@ -62,7 +63,9 @@ export function reset() {
* recognize additional tokens or groups.
*/
export function registerTokenPlugin(name, plugin) {
if (typeof plugin !== 'function') { throw new Error(`linkifyjs: Invalid token plugin ${plugin} (expects function)`); }
if (typeof plugin !== 'function') {
throw new Error(`linkifyjs: Invalid token plugin ${plugin} (expects function)`);
}
for (let i = 0; i < INIT.tokenQueue.length; i++) {
if (name === INIT.tokenQueue[i][0]) {
warn(`linkifyjs: token plugin "${name}" already registered - will be overwritten`);
Expand All @@ -83,7 +86,9 @@ export function registerTokenPlugin(name, plugin) {
* extends the parser to recognize additional link types
*/
export function registerPlugin(name, plugin) {
if (typeof plugin !== 'function') { throw new Error(`linkifyjs: Invalid plugin ${plugin} (expects function)`); }
if (typeof plugin !== 'function') {
throw new Error(`linkifyjs: Invalid plugin ${plugin} (expects function)`);
}
for (let i = 0; i < INIT.pluginQueue.length; i++) {
if (name === INIT.pluginQueue[i][0]) {
warn(`linkifyjs: plugin "${name}" already registered - will be overwritten`);
Expand All @@ -109,7 +114,10 @@ export function registerCustomProtocol(scheme, optionalSlashSlash = false) {
warn(`linkifyjs: already initialized - will not register custom scheme "${scheme}" ${warnAdvice}`);
}
if (!/^[0-9a-z]+(-[0-9a-z]+)*$/.test(scheme)) {
throw new Error('linkifyjs: incorrect scheme format.\n 1. Must only contain digits, lowercase ASCII letters or "-"\n 2. Cannot start or end with "-"\n 3. "-" cannot repeat');
throw new Error(`linkifyjs: incorrect scheme format.
1. Must only contain digits, lowercase ASCII letters or "-"
2. Cannot start or end with "-"
3. "-" cannot repeat`);
}
INIT.customSchemes.push([scheme, optionalSlashSlash]);
}
Expand All @@ -123,7 +131,7 @@ export function init() {
INIT.scanner = initScanner(INIT.customSchemes);
for (let i = 0; i < INIT.tokenQueue.length; i++) {
INIT.tokenQueue[i][1]({
scanner: INIT.scanner
scanner: INIT.scanner,
});
}

Expand All @@ -144,7 +152,9 @@ export function init() {
* @return {MultiToken[]} tokens
*/
export function tokenize(str) {
if (!INIT.initialized) { init(); }
if (!INIT.initialized) {
init();
}
return runParser(INIT.parser.start, str, runScanner(INIT.scanner.start, str));
}

Expand All @@ -155,7 +165,7 @@ export function tokenize(str) {
* links to find, e.g., 'url' or 'email'
* @param {Opts} [opts] formatting options for final output. Cannot be specified
* if opts already provided in `type` argument
*/
*/
export function find(str, type = null, opts = null) {
if (type && typeof type === 'object') {
if (opts) {
Expand All @@ -170,7 +180,7 @@ export function find(str, type = null, opts = null) {

for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.isLink && (!type || token.t === type)) {
if (token.isLink && (!type || token.t === type) && options.check(token)) {
filtered.push(token.toFormattedObject(options));
}
}
Expand All @@ -196,9 +206,7 @@ export function find(str, type = null, opts = null) {
*/
export function test(str, type = null) {
const tokens = tokenize(str);
return tokens.length === 1 && tokens[0].isLink && (
!type || tokens[0].t === type
);
return tokens.length === 1 && tokens[0].isLink && (!type || tokens[0].t === type);
}

export * as options from './options';
Expand Down
17 changes: 11 additions & 6 deletions packages/linkifyjs/src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const defaults = {
className: null,
attributes: null,
ignoreTags: [],
render: null
render: null,
};

/**
Expand All @@ -109,9 +109,10 @@ export const defaults = {
* Similar to render option
*/
export function Options(opts, defaultRender = null) {

let o = assign({}, defaults);
if (opts) { o = assign(o, opts instanceof Options ? opts.o : opts); }
if (opts) {
o = assign(o, opts instanceof Options ? opts.o : opts);
}

// Ensure all ignored tags are uppercase
const ignoredTags = o.ignoreTags;
Expand All @@ -121,7 +122,9 @@ export function Options(opts, defaultRender = null) {
}
/** @protected */
this.o = o;
if (defaultRender) { this.defaultRender = defaultRender; }
if (defaultRender) {
this.defaultRender = defaultRender;
}
this.ignoreTags = uppercaseIgnoredTags;
}

Expand Down Expand Up @@ -167,7 +170,9 @@ Options.prototype = {
get(key, operator, token) {
const isCallable = operator != null;
let option = this.o[key];
if (!option) { return option; }
if (!option) {
return option;
}
if (typeof option === 'object') {
option = token.t in option ? option[token.t] : defaults[key];
if (typeof option === 'function' && isCallable) {
Expand Down Expand Up @@ -206,7 +211,7 @@ Options.prototype = {
const ir = token.render(this); // intermediate representation
const renderFn = this.get('render', null, token) || this.defaultRender;
return renderFn(ir, token.t, token);
}
},
};

export { assign };
Expand Down
42 changes: 21 additions & 21 deletions test/spec/linkify-jquery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ if (!doc) {
}

describe('linkify-jquery', function () {

// Sometimes jQuery is slow to load
this.timeout(10000);

Expand All @@ -24,9 +23,7 @@ describe('linkify-jquery', function () {
This code allows testing on Node.js and on Browser environments
*/
before(function (done) {

function onDoc($, doc) {

doc.body.innerHTML = htmlOptions.extra;

// Add the linkify plugin to jQuery
Expand All @@ -40,13 +37,18 @@ describe('linkify-jquery', function () {
done();
}

if (doc) { return onDoc($, doc); }
if (doc) {
return onDoc($, doc);
}
// no document element, use a virtual dom to test

let dom = new JSDOM('<html><head><title>Linkify Test</title></head><body><script src="https://code.jquery.com/jquery.js"></script></body></html>', {
runScripts: 'dangerously',
resources: 'usable'
});
let dom = new JSDOM(
'<html><head><title>Linkify Test</title></head><body><script src="https://code.jquery.com/jquery.js"></script></body></html>',
{
runScripts: 'dangerously',
resources: 'usable',
},
);
doc = dom.window.document;
dom.window.onload = () => {
$ = dom.window.jQuery;
Expand All @@ -55,44 +57,42 @@ describe('linkify-jquery', function () {
});

// Make sure we start out with a fresh DOM every time
beforeEach(() => testContainer.innerHTML = htmlOptions.original);
beforeEach(() => (testContainer.innerHTML = htmlOptions.original));

it('Works with the DOM Data API', () => {
expect($('header').first().html()).to.be.eql(
'Have a link to:<br><a href="https://github.com">github.com</a>!'
);
expect($('header').first().html()).to.be.eql('Have a link to:<br><a href="https://github.com">github.com</a>!');
expect($('#linkify-test-div').html()).to.be.eql(
'Another <i href="mailto:test@gmail.com" class="test-class" ' +
'target="_parent">test@gmail.com</i> email as well as a <i '+
'href="http://t.co" class="test-class" target="_parent">' +
'http://t.co</i> link.'
'target="_parent">test@gmail.com</i> email as well as a <i ' +
'href="http://t.co" class="test-class" target="_parent">' +
'http://t.co</i> link.',
);
});

it('Works with default options', () => {
var $container = $('#linkify-jquery-test-container');
expect(($container.length)).to.be.eql(1);
expect($container.length).to.be.eql(1);
var result = $container.linkify();
// `should` is not defined on jQuery objects
expect((result === $container)).to.be.ok; // should return the same element
expect(result === $container).to.be.ok; // should return the same element
expect($container.html()).to.be.oneOf(htmlOptions.linkified);
});

it('Works with overriden options (general)', () => {
var $container = $('#linkify-jquery-test-container');
expect(($container.length)).to.be.eql(1);
expect($container.length).to.be.eql(1);
var result = $container.linkify(htmlOptions.altOptions);
// `should` is not defined on jQuery objects
expect((result === $container)).to.be.ok; // should return the same element
expect(result === $container).to.be.ok; // should return the same element
expect($container.html()).to.be.oneOf(htmlOptions.linkifiedAlt);
});

it('Works with overriden options (validate)', () => {
var $container = $('#linkify-jquery-test-container');
expect(($container.length)).to.be.eql(1);
expect($container.length).to.be.eql(1);
var result = $container.linkify(htmlOptions.validateOptions);
// `should` is not defined on jQuery objects
expect((result === $container)).to.be.ok; // should return the same element
expect(result === $container).to.be.ok; // should return the same element
expect($container.html()).to.be.oneOf(htmlOptions.linkifiedValidate);
});
});
73 changes: 51 additions & 22 deletions test/spec/linkifyjs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,25 +94,29 @@ describe('linkifyjs', () => {
});

it('Find the link', () => {
expect(linkify.find('hello.world!')).to.deep.eql([{
type: 'url',
value: 'hello.world',
href: 'http://hello.world',
isLink: true,
start: 0,
end: 11
}]);
expect(linkify.find('hello.world!')).to.deep.eql([
{
type: 'url',
value: 'hello.world',
href: 'http://hello.world',
isLink: true,
start: 0,
end: 11,
},
]);
});

it('Find the link of the specific type', () => {
expect(linkify.find('For help with github.com, please contact support@example.com', 'email')).to.deep.eql([{
type: 'email',
value: 'support@example.com',
href: 'mailto:support@example.com',
isLink: true,
start: 41,
end: 60
}]);
expect(linkify.find('For help with github.com, please contact support@example.com', 'email')).to.deep.eql([
{
type: 'email',
value: 'support@example.com',
href: 'mailto:support@example.com',
isLink: true,
start: 41,
end: 60,
},
]);
});

it('Finds with opts', () => {
Expand All @@ -123,27 +127,52 @@ describe('linkifyjs', () => {
isLink: true,
href: 'http://www.truncate.com',
start: 5,
end: 21
}
end: 21,
},
]);
});

it('Finds type and opts', () => {
expect(linkify.find('Does www.truncate.com work with example@truncate.com?', 'email', { truncate: 10 })).to.deep.eql([
expect(
linkify.find('Does www.truncate.com work with example@truncate.com?', 'email', { truncate: 10 }),
).to.deep.eql([
{
type: 'email',
value: 'example@tr…',
isLink: true,
href: 'mailto:example@truncate.com',
start: 32,
end: 52
}
end: 52,
},
]);
});

it('Throws on ambiguous invocation', () => {
expect(() => linkify.find('Hello.com', { type: 'email' }, { truncate: 10 })).to.throw();
});

it('Uses validation to ignore links', () => {
expect(
linkify.find('foo.com and bar.com and baz.com', { validate: (url) => url !== 'bar.com' }),
).to.deep.eql([
{
type: 'url',
value: 'foo.com',
isLink: true,
href: 'http://foo.com',
start: 0,
end: 7,
},
{
end: 31,
href: 'http://baz.com',
isLink: true,
start: 24,
type: 'url',
value: 'baz.com',
},
]);
});
});

describe('test', () => {
Expand All @@ -167,7 +196,7 @@ describe('linkifyjs', () => {
['mailto:test+5@uwaterloo.ca', true, 'url'],
['t.co', true],
['t.co g.co', false], // can only be one
['test@g.co t.co', false] // can only be one
['test@g.co t.co', false], // can only be one
];

it('is a function', () => {
Expand Down

0 comments on commit 17406b8

Please sign in to comment.