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

Support custom rules #20

Closed
wants to merge 8 commits into from
40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`


Expand All @@ -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
Expand Down
76 changes: 72 additions & 4 deletions src/Linkify.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we move this.linkify = new LinkifiyIt(); into componentDidMount() so that this.linkify will always be defined when calling addCustomHandlers().

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) => {
Copy link
Owner

@tasti tasti Jun 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handlers defaults to [], so handlers.forEach(...); should be sufficient.

this.linkify.add(handler.prefix, {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens to handlers that are removed in the new props? Removed handlers should be set to null to disable them (see https://github.com/markdown-it/linkify-it#addschema-definition).

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) {
Expand Down
170 changes: 170 additions & 0 deletions src/__tests__/Linkify-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Linkify></Linkify>);
Expand Down Expand Up @@ -121,6 +123,174 @@ describe('Linkify', () => {
});
});

describe('LinkifyIt config', () => {
it('should match a custom handler added through the "handlers" prop', () => {
const linkify = TestUtils.renderIntoDocument(
<Linkify handlers={[{
prefix: '@',
validate() {
return 7;
},
normalize(match) {
match.url = 'https://twitter.com/' + match.url.replace(/^@/, '');
}
}]}
>
</Linkify>
);

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(
<Linkify handlers={[{
prefix: '@',
validate() {
return 7;
},
normalize(match) {
match.url = 'https://twitter.com/' + match.url.replace(/^@/, '');
}
}, {
prefix: '$',
validate() {
return 7;
},
normalize(match) {
match.url = 'https://blingtwitter.com/' + match.url.replace(/^\$/, '');
}
}]}
>
</Linkify>
);

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(
<Linkify />
);

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(
<Linkify handlers={[{
prefix: '$',
validate() {
return 7;
},
normalize(match) {
match.url = 'https://blingtwitter.com/' + match.url.replace(/^\$/, '');
}
}]}
>
</Linkify>
);

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(
<Linkify fuzzyLink={ false } />
);

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(
<Linkify />
);

const input = 'this @mention should not match';
const output = linkify.parseString(input);

expect(output).toEqual(input);
})
});

describe('#render', () => {
let linkify = TestUtils.renderIntoDocument(<Linkify></Linkify>);

Expand Down