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 diff --git a/src/Linkify.jsx b/src/Linkify.jsx index 8f86702..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'; -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' @@ -13,19 +38,62 @@ 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.isRequired, + validate: React.PropTypes.func.isRequired, + normalize: React.PropTypes.func.isRequired + }) + ), + fuzzyLink: React.PropTypes.bool, + fuzzyIP: React.PropTypes.bool, + fuzzyEmail: React.PropTypes.bool } static defaultProps = { className: 'Linkify', component: 'a', properties: {}, + handlers: [] + } + + componentDidMount() { + this.addCustomHandlers(); + } + + componentDidUpdate(nextProps) { + this.addCustomHandlers(); + } + + addCustomHandlers() { + const { handlers } = this.props; + + this.linkify = this.linkify || new LinkifyIt(); + this.linkify.tlds(tlds); + + // 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) { - return linkify.match(string); + return this.linkify.match(string); } parseString(string) { diff --git a/src/__tests__/Linkify-test.js b/src/__tests__/Linkify-test.js index 196384c..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,6 +123,174 @@ describe('Linkify', () => { }); }); + describe('LinkifyIt config', () => { + it('should match a custom handler 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]); + }); + + 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]); + }) + + 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', () => { let linkify = TestUtils.renderIntoDocument();