From 638646dfd8feef786c7ae808687b73bfa334311b Mon Sep 17 00:00:00 2001 From: Mitch Robb Date: Thu, 26 May 2016 15:26:28 -0700 Subject: [PATCH 1/7] support custom linkify-it handlers; export linkify-it instance and add handlers prop --- src/Linkify.jsx | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Linkify.jsx b/src/Linkify.jsx index 1f5173c..b014dc0 100644 --- a/src/Linkify.jsx +++ b/src/Linkify.jsx @@ -2,7 +2,7 @@ import React from 'react'; import LinkifyIt from 'linkify-it'; import tlds from 'tlds'; -const linkify = new LinkifyIt(); +export const linkify = new LinkifyIt(); linkify.tlds(tlds); class Linkify extends React.Component { @@ -12,12 +12,43 @@ class Linkify extends React.Component { component: React.PropTypes.any, properties: React.PropTypes.object, urlRegex: React.PropTypes.object, - emailRegex: React.PropTypes.object + emailRegex: React.PropTypes.object, + handlers: React.PropTypes.arrayOf( + React.PropTypes.shape({ + prefix: React.PropTypes.string, + validate: React.PropTypes.func, + normalize: React.PropTypes.func + }) + ) } static defaultProps = { component: 'a', properties: {}, + handlers: [] + } + + componentDidMount() { + this.addCustomHandlers() + } + + componentDidUpdate(nextProps) { + if (this.props.handlers !== nextProps.handlers) { + this.addCustomHandlers() + } + } + + addCustomHandlers() { + const { handlers } = this.props + + if (handlers.length) { + handlers.forEach(handler => { + linkify.add(handler.prefix, { + validate: handler.validate, + normalize: handler.normalize + }) + }) + } } parseCounter = 0 From 3bea2b40e87bcf01c7b97b3e9784924b21fa5f95 Mon Sep 17 00:00:00 2001 From: Thomas Ladd Date: Thu, 26 May 2016 16:42:41 -0500 Subject: [PATCH 2/7] Upgrade babel-jest, jest-cli, and jest Require default Linkify in tests --- package.json | 6 +++--- src/__tests__/Linkify-test.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index b49daf9..312e8d9 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,12 @@ }, "devDependencies": { "babel-cli": "^6.7.7", - "babel-jest": "~5.3.0", + "babel-jest": "^12.1.0", "babel-plugin-transform-react-jsx": "^6.7.5", "babel-preset-es2015": "^6.6.0", "babel-preset-stage-0": "^6.5.0", - "jest": "^0.1.40", - "jest-cli": "~0.7.1", + "jest": "^12.1.1", + "jest-cli": "^12.1.1", "react-addons-test-utils": "~0.14.2" }, "jest": { diff --git a/src/__tests__/Linkify-test.js b/src/__tests__/Linkify-test.js index e27e3d3..3079d7f 100644 --- a/src/__tests__/Linkify-test.js +++ b/src/__tests__/Linkify-test.js @@ -4,7 +4,7 @@ let React = require('react'); let TestUtils = require('react-addons-test-utils'); describe('Linkify', () => { - let Linkify = require('../Linkify.jsx'); + let Linkify = require('../Linkify.jsx').default; describe('#parseString', () => { let linkify = TestUtils.renderIntoDocument(); From 299600b6b8383279a0b2eb4887b38d69e1e25800 Mon Sep 17 00:00:00 2001 From: Mitch Robb Date: Thu, 26 May 2016 15:52:28 -0700 Subject: [PATCH 3/7] add test for handlers prop. make handler object keys required --- src/Linkify.jsx | 6 +++--- src/__tests__/Linkify-test.js | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Linkify.jsx b/src/Linkify.jsx index b014dc0..2fcdd47 100644 --- a/src/Linkify.jsx +++ b/src/Linkify.jsx @@ -15,9 +15,9 @@ class Linkify extends React.Component { emailRegex: React.PropTypes.object, handlers: React.PropTypes.arrayOf( React.PropTypes.shape({ - prefix: React.PropTypes.string, - validate: React.PropTypes.func, - normalize: React.PropTypes.func + prefix: React.PropTypes.string.isRequired, + validate: React.PropTypes.func.isRequired, + normalize: React.PropTypes.func.isRequired }) ) } diff --git a/src/__tests__/Linkify-test.js b/src/__tests__/Linkify-test.js index 3079d7f..413d261 100644 --- a/src/__tests__/Linkify-test.js +++ b/src/__tests__/Linkify-test.js @@ -121,6 +121,33 @@ describe('Linkify', () => { }); }); + describe('#addCustomHandlers', () => { + it('should match all custom handlers added through the "handlers" prop', () => { + const linkify = TestUtils.renderIntoDocument( + + + ); + + const input = ['this is an ', '@mention', ' handler']; + const output = linkify.parseString(input.join('')); + + expect(output[0]).toEqual(input[0]); + expect(output[1].type).toEqual('a'); + expect(output[1].props.href).toEqual(`https://twitter.com/mention`); + expect(output[1].props.children).toEqual(input[1]); + expect(output[2]).toEqual(input[2]); + }) + }); + describe('#render', () => { }); From 8c72dd8b57c6dec755240bc6d9a1371eb5fd4432 Mon Sep 17 00:00:00 2001 From: Mitch Robb Date: Thu, 26 May 2016 16:11:45 -0700 Subject: [PATCH 4/7] add README entry --- README.md | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1dec11c..a2dbc3e 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ If you're feeling lazy, you can wrap `Linkify` around anywhere that you want lin Renders to: -react-linkify (`tasti.github.io/react-linkify/`) -React component to parse links (urls, emails, etc.) in text into clickable links -See examples at `tasti.github.io/react-linkify/`. +react-linkify (`tasti.github.io/react-linkify/`) +React component to parse links (urls, emails, etc.) in text into clickable links +See examples at `tasti.github.io/react-linkify/`. Contact: `tasti@zakarie.com` @@ -57,18 +57,38 @@ React.render( ## Props -**component** -The type of component to wrap links in. -_type:_ `any` -_default:_ `'a'` +**component** +The type of component to wrap links in. +_type:_ `any` +_default:_ `'a'` -**properties** -The props that will be added to every matched component. -_type:_ `object` +**properties** +The props that will be added to every matched component. +_type:_ `object` _default:_ `{}` NOTE: Use `Linkify.MATCH` as a value to specify the matched link. The properties prop will always contain `{href: Linkify.MATCH, key: 'LINKIFY_KEY_#'}` unless overridden. +**handlers** +Handlers to match custom link types, like Twitter @mentions. + +_type_: +```js +arrayOf( + shape({ + prefix: string.isRequired, + validate: func.isRequired, + normalize: func.isRequired + }) +) +``` +_default:_ `[]` + +See the [example mentions handler](https://github.com/markdown-it/linkify-it#example-2-add-twitter-mentions-handler) from linkify-it for more details. + +## Customization +Custom handlers can be added to a specific `Linkify` instance with the `handlers` prop. Additionally, the singleton linkify-it instance can be imported (`import { linkify } from 'react-linkify'`) for global customization. See the [linkify-it api docs](http://markdown-it.github.io/linkify-it/doc/) + ## Examples All kind of links detectable by From 2cf3e99754e2a9e903aeeba046920f572a416ebf Mon Sep 17 00:00:00 2001 From: Mitch Robb Date: Thu, 26 May 2016 16:22:14 -0700 Subject: [PATCH 5/7] add test for multiple custom handlers --- src/__tests__/Linkify-test.js | 40 ++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/__tests__/Linkify-test.js b/src/__tests__/Linkify-test.js index 413d261..ac79c7c 100644 --- a/src/__tests__/Linkify-test.js +++ b/src/__tests__/Linkify-test.js @@ -122,7 +122,7 @@ describe('Linkify', () => { }); describe('#addCustomHandlers', () => { - it('should match all custom handlers added through the "handlers" prop', () => { + it('should match a custom handler added through the "handlers" prop', () => { const linkify = TestUtils.renderIntoDocument( { expect(output[1].props.href).toEqual(`https://twitter.com/mention`); expect(output[1].props.children).toEqual(input[1]); expect(output[2]).toEqual(input[2]); + }); + + it('should match multiple custom handlers', () => { + const linkify = TestUtils.renderIntoDocument( + + + ); + + const input = ['this is an ', '@mention', ' and ', '$mention', ' handler']; + const output = linkify.parseString(input.join('')); + + expect(output[0]).toEqual(input[0]); + expect(output[1].type).toEqual('a'); + expect(output[1].props.href).toEqual(`https://twitter.com/mention`); + expect(output[1].props.children).toEqual(input[1]); + + expect(output[2]).toEqual(input[2]); + expect(output[3].type).toEqual('a'); + expect(output[3].props.href).toEqual(`https://blingtwitter.com/mention`); + expect(output[3].props.children).toEqual(input[3]); + expect(output[4]).toEqual(input[4]); }) }); From d8b7ac40a15ede8f385832a2f0e1e74e43d32c72 Mon Sep 17 00:00:00 2001 From: Mitch Robb Date: Thu, 26 May 2016 17:09:41 -0700 Subject: [PATCH 6/7] use local linkifyIt if adding handlers, to avoid polluting the global instance add ;s syntax cleanup --- src/Linkify.jsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Linkify.jsx b/src/Linkify.jsx index 2fcdd47..53841e7 100644 --- a/src/Linkify.jsx +++ b/src/Linkify.jsx @@ -29,32 +29,37 @@ class Linkify extends React.Component { } componentDidMount() { - this.addCustomHandlers() + this.addCustomHandlers(); } componentDidUpdate(nextProps) { if (this.props.handlers !== nextProps.handlers) { - this.addCustomHandlers() + this.addCustomHandlers(); } } addCustomHandlers() { - const { handlers } = this.props + const { handlers } = this.props; if (handlers.length) { + this.linkify = new LinkifyIt(); + this.linkify.tlds(tlds); + handlers.forEach(handler => { - linkify.add(handler.prefix, { + this.linkify.add(handler.prefix, { validate: handler.validate, normalize: handler.normalize - }) - }) + }); + }); } } parseCounter = 0 getMatches(string) { - return linkify.match(string); + const linkifyInstance = this.linkify || linkify; + + return linkifyInstance.match(string); } parseString(string) { From 35512d0d654aa36201b8348cbe33990addf09539 Mon Sep 17 00:00:00 2001 From: Mitch Robb Date: Fri, 3 Jun 2016 00:57:40 -0700 Subject: [PATCH 7/7] first stab at merging global and instance linkifyIt customizations --- src/Linkify.jsx | 68 +++++++++++++++------ src/__tests__/Linkify-test.js | 107 +++++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 19 deletions(-) diff --git a/src/Linkify.jsx b/src/Linkify.jsx index 4142e12..6ed4a9f 100644 --- a/src/Linkify.jsx +++ b/src/Linkify.jsx @@ -2,8 +2,33 @@ import React from 'react'; import LinkifyIt from 'linkify-it'; import tlds from 'tlds'; -export const linkify = new LinkifyIt(); -linkify.tlds(tlds); +const globalCustomizations = { + add: [], + tlds: [], + set: [] +}; + +export const config = { + add: (...args) => { + globalCustomizations.add.push(args); + return config; + }, + tlds: (...args) => { + globalCustomizations.tlds.push(args); + return config; + }, + set: (...args) => { + globalCustomizations.set.push(args); + return config; + }, + resetAll: () => { + for (let type in globalCustomizations) { + globalCustomizations[type] = []; + } + + return config; + } +}; class Linkify extends React.Component { static MATCH = 'LINKIFY_MATCH' @@ -20,7 +45,10 @@ class Linkify extends React.Component { validate: React.PropTypes.func.isRequired, normalize: React.PropTypes.func.isRequired }) - ) + ), + fuzzyLink: React.PropTypes.bool, + fuzzyIP: React.PropTypes.bool, + fuzzyEmail: React.PropTypes.bool } static defaultProps = { @@ -35,33 +63,37 @@ class Linkify extends React.Component { } componentDidUpdate(nextProps) { - if (this.props.handlers !== nextProps.handlers) { - this.addCustomHandlers(); - } + this.addCustomHandlers(); } addCustomHandlers() { const { handlers } = this.props; - if (handlers.length) { - this.linkify = new LinkifyIt(); - this.linkify.tlds(tlds); + this.linkify = this.linkify || new LinkifyIt(); + this.linkify.tlds(tlds); - handlers.forEach(handler => { - this.linkify.add(handler.prefix, { - validate: handler.validate, - normalize: handler.normalize - }); - }); + // add global customizations + for (let type in globalCustomizations) { + globalCustomizations[type].forEach(c => this.linkify[type](...c)) } + + // add instance customizations + (handlers || []).forEach((handler) => { + this.linkify.add(handler.prefix, { + validate: handler.validate, + normalize: handler.normalize + }); + }); + + ['fuzzyLink', 'fuzzyIP', 'fuzzyEmail'].forEach(f => { + typeof this.props[f] === 'boolean' && this.linkify.set({ [f]: this.props[f] }) + }) } parseCounter = 0 getMatches(string) { - const linkifyInstance = this.linkify || linkify; - - return linkifyInstance.match(string); + return this.linkify.match(string); } parseString(string) { diff --git a/src/__tests__/Linkify-test.js b/src/__tests__/Linkify-test.js index 4f2578d..528a986 100644 --- a/src/__tests__/Linkify-test.js +++ b/src/__tests__/Linkify-test.js @@ -5,6 +5,8 @@ let TestUtils = require('react-addons-test-utils'); describe('Linkify', () => { let Linkify = require('../Linkify.jsx').default; + let linkifyCustomizations = require('../Linkify.jsx').config; + describe('#parseString', () => { let linkify = TestUtils.renderIntoDocument(); @@ -121,7 +123,7 @@ describe('Linkify', () => { }); }); - describe('#addCustomHandlers', () => { + describe('LinkifyIt config', () => { it('should match a custom handler added through the "handlers" prop', () => { const linkify = TestUtils.renderIntoDocument( { expect(output[3].props.children).toEqual(input[3]); expect(output[4]).toEqual(input[4]); }) + + it('should apply global customizations', () => { + linkifyCustomizations + .resetAll() + .tlds('linkify', true) + .add('@', { + validate() { + return 7; + }, + normalize(match) { + match.url = 'https://twitter.com/' + match.url.replace(/^@/, ''); + } + }) + const linkify = TestUtils.renderIntoDocument( + + ); + + const input = ['this is an ', '@mention', ' and ', 'test.linkify', ' TLD handler']; + const output = linkify.parseString(input.join('')); + + expect(output[0]).toEqual(input[0]); + expect(output[1].type).toEqual('a'); + expect(output[1].props.href).toEqual(`https://twitter.com/mention`); + expect(output[1].props.children).toEqual(input[1]); + + expect(output[2]).toEqual(input[2]); + expect(output[3].type).toEqual('a'); + expect(output[3].props.href).toEqual(`http://test.linkify`); + expect(output[3].props.children).toEqual(input[3]); + expect(output[4]).toEqual(input[4]); + }); + + it('should merge global and instance handlers', () => { + linkifyCustomizations + .resetAll() + .add('@', { + validate() { + return 7; + }, + normalize(match) { + match.url = 'https://twitter.com/' + match.url.replace(/^@/, ''); + } + }) + const linkify = TestUtils.renderIntoDocument( + + + ); + + const input = ['this is an ', '@mention', ' and ', '$mention', ' handler']; + const output = linkify.parseString(input.join('')); + + expect(output[0]).toEqual(input[0]); + expect(output[1].type).toEqual('a'); + expect(output[1].props.href).toEqual(`https://twitter.com/mention`); + expect(output[1].props.children).toEqual(input[1]); + + expect(output[2]).toEqual(input[2]); + expect(output[3].type).toEqual('a'); + expect(output[3].props.href).toEqual(`https://blingtwitter.com/mention`); + expect(output[3].props.children).toEqual(input[3]); + expect(output[4]).toEqual(input[4]); + }); + + it('should set fuzzy* options', () => { + const linkify = TestUtils.renderIntoDocument( + + ); + + const linkInput = 'this should not match: www.test.com'; + const linkOutput = linkify.parseString(linkInput); + expect(linkOutput).toEqual(linkInput); + }); + + it('should reset global customizations', () => { + linkifyCustomizations + .add('@', { + validate() { + return 7; + }, + normalize(match) { + match.url = 'https://twitter.com/' + match.url.replace(/^@/, ''); + } + }) + .resetAll() + + const linkify = TestUtils.renderIntoDocument( + + ); + + const input = 'this @mention should not match'; + const output = linkify.parseString(input); + + expect(output).toEqual(input); + }) }); describe('#render', () => {