Skip to content
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ var Highlight = require('react-highlighter');
## Props
- `search`: The string of text (or Regular Expression) to highlight
- `caseSensitive`: Determine whether string matching should be case-sensitive. Not applicable to regular expression searches. Defaults to `false`
- `matchElement`: HTML tag name to wrap around highlighted text. Defaults to `strong`
- `ignoreDiacritics`: Determine whether string matching should ignore diacritics. Defaults to `false`
- `diacriticsBlacklist`: These chars are treated like characters that don't have any diacritics. Not applicable ignoreDiacritics is `false`. Defaults to none
- `matchElement`: HTML tag name to wrap around highlighted text. Defaults to `mark`
- `matchClass`: HTML class to wrap around highlighted text. Defaults to `highlight`
- `matchStyle`: Custom style for the match element around highlighted text.

Expand Down
50 changes: 45 additions & 5 deletions lib/highlighter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ var blacklist = require('blacklist');
var createReactClass = require('create-react-class');
var PropTypes = require('prop-types');

function removeDiacritics(str, blacklist) {
if (!String.prototype.normalize) {
Copy link
Contributor

@ericgio ericgio Jan 14, 2018

Choose a reason for hiding this comment

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

The use of normalize pretty much excludes IE. This solution is more widely cross-browser compatible.

// Fall back to original string
return str;
}

if (!blacklist) {
// No blacklist, just remove all
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
else {
var blacklistChars = blacklist.split('');

// Remove all diacritics that are not a part of a blacklisted character
// First char cannot be a diacritic
return str.normalize('NFD').replace(/.[\u0300-\u036f]+/g, function(m) {
return blacklistChars.indexOf(m.normalize()) > -1 ? m.normalize() : m[0];
});
}
}

var Highlighter = createReactClass({
displayName: 'Highlighter',
count: 0,
Expand All @@ -16,6 +37,8 @@ var Highlighter = createReactClass({
RegExpPropType
]).isRequired,
caseSensitive: PropTypes.bool,
ignoreDiacritics: PropTypes.bool,
diacriticsBlacklist: PropTypes.string,
matchElement: PropTypes.string,
matchClass: PropTypes.string,
matchStyle: PropTypes.object
Expand All @@ -26,6 +49,8 @@ var Highlighter = createReactClass({
this.props,
'search',
'caseSensitive',
'ignoreDiacritics',
'diacriticsBlacklist',
'matchElement',
'matchClass',
'matchStyle'
Expand Down Expand Up @@ -92,6 +117,10 @@ var Highlighter = createReactClass({
search = escapeStringRegexp(search);
}

if (this.props.ignoreDiacritics) {
search = removeDiacritics(search, this.props.diacriticsBlacklist);
}

return new RegExp(search, flags);
},

Expand Down Expand Up @@ -131,16 +160,25 @@ var Highlighter = createReactClass({
*/
highlightChildren: function(subject, search) {
var children = [];
var matchElement = this.props.matchElement;
var remaining = subject;

while (remaining) {
if (!search.test(remaining)) {
var remainingCleaned = (this.props.ignoreDiacritics
? removeDiacritics(remaining, this.props.diacriticsBlacklist)
: remaining
);

if (!search.test(remainingCleaned)) {
children.push(this.renderPlain(remaining));
return children;
}

var boundaries = this.getMatchBoundaries(remaining, search);
var boundaries = this.getMatchBoundaries(remainingCleaned, search);

if (boundaries.first === 0 && boundaries.last === 0) {
// Regex zero-width match
return children;
}

// Capture the string that leads up to a match...
var nonMatch = remaining.slice(0, boundaries.first);
Expand All @@ -151,7 +189,7 @@ var Highlighter = createReactClass({
// Now, capture the matching string...
var match = remaining.slice(boundaries.first, boundaries.last);
if (match) {
children.push(this.renderHighlight(match, matchElement));
children.push(this.renderHighlight(match));
}

// And if there's anything left over, recursively run this method again.
Expand Down Expand Up @@ -196,7 +234,9 @@ var Highlighter = createReactClass({

Highlighter.defaultProps = {
caseSensitive: false,
matchElement: 'strong',
ignoreDiacritics: false,
diacriticsBlacklist: '',
matchElement: 'mark',
matchClass: 'highlight',
matchStyle: {}
};
Expand Down
125 changes: 121 additions & 4 deletions test/testHighlighter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('Highlight element', function() {
it('is what it says it is', function() {
var element = React.createElement(Highlight, {search: 'world'}, 'Hello World');
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'strong');
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');

expect(TestUtils.isElement(element)).to.be.true;
expect(TestUtils.isElementOfType(element, Highlight)).to.be.true;
Expand Down Expand Up @@ -57,28 +57,145 @@ describe('Highlight element', function() {
it('should support custom style for matching element', function() {
var element = React.createElement(Highlight, {search: 'Seek', matchStyle: { color: 'red' }}, 'Hide and Seek');
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'strong');
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches[0].getAttribute('style')).to.eql('color: red;');
});

it('should support passing props to parent element', function() {
var element = React.createElement(Highlight, {search: 'world', className: 'myHighlighter'}, 'Hello World');
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'strong');
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');

expect(ReactDOM.findDOMNode(node).className).to.equal('myHighlighter');
expect(ReactDOM.findDOMNode(matches[0]).className).to.equal('highlight')
});

it('should support case sensitive searches', function() {
var element = React.createElement(Highlight, {search: 'TEST', caseSensitive: true}, 'test Test TeSt TEST');
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(1);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('TEST');
});

it('should support matching diacritics exactly', function() {
var text = 'Café has a weird e. Cafééééé has five of them. Cafe has a normal e. Cafeeeee has five of them.';
var element = React.createElement(Highlight, {search: 'Cafe'}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(2);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('Cafe');
expect(ReactDOM.findDOMNode(matches[1]).textContent).to.equal('Cafe');

var element = React.createElement(Highlight, {search: 'Café'}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(2);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('Café');
expect(ReactDOM.findDOMNode(matches[1]).textContent).to.equal('Café');
});

it('should support ignoring diacritics', function() {
var text = 'Café has a weird e. Cafééééé has five of them. Cafe has a normal e. Cafeeeee has five of them.';
var element = React.createElement(Highlight, {search: 'Cafe', ignoreDiacritics: true}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(4);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('Café');
expect(ReactDOM.findDOMNode(matches[1]).textContent).to.equal('Café');
expect(ReactDOM.findDOMNode(matches[2]).textContent).to.equal('Cafe');
expect(ReactDOM.findDOMNode(matches[3]).textContent).to.equal('Cafe');
});

it('should support regular expressions in search', function() {
var element = React.createElement(Highlight, {search: /[A-Za-z]+/}, 'Easy as 123, ABC...');
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'strong');
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('Easy');
expect(ReactDOM.findDOMNode(matches[1]).textContent).to.equal('as');
expect(ReactDOM.findDOMNode(matches[2]).textContent).to.equal('ABC');
});

it('should work when regular expressions in search do not match anything', function() {
var element = React.createElement(Highlight, {search: /z+/}, 'Easy as 123, ABC...');
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(0);
});

it('should stop immediately if regex matches an empty string', function() {
var element = React.createElement(Highlight, {search: /z*/}, 'Ez as 123, ABC...');
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(0);

var element = React.createElement(Highlight, {search: /z*/}, 'zzz Ez as 123, ABC...');
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(1);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('zzz');
});

it('should support matching diacritics exactly with regex', function() {
var text = 'Café has a weird e. Cafééééé has five of them. Cafe has a normal e. Cafeeeee has five of them.';
var element = React.createElement(Highlight, {search: /Cafe/}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(2);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('Cafe');
expect(ReactDOM.findDOMNode(matches[1]).textContent).to.equal('Cafe');

var element = React.createElement(Highlight, {search: /Café/}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(2);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('Café');
expect(ReactDOM.findDOMNode(matches[1]).textContent).to.equal('Café');
});

it('should support ignoring diacritics with regex', function() {
var text = 'Café has a weird e. Cafééééé has five of them. Cafe has a normal e. Cafeeeee has five of them.';
var element = React.createElement(Highlight, {search: /Cafe+/, ignoreDiacritics: true}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(4);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('Café');
expect(ReactDOM.findDOMNode(matches[1]).textContent).to.equal('Cafééééé');
expect(ReactDOM.findDOMNode(matches[2]).textContent).to.equal('Cafe');
expect(ReactDOM.findDOMNode(matches[3]).textContent).to.equal('Cafeeeee');
});

it('should support ignoring diacritics with blacklist', function() {
var text = 'Letter ä is a normal letter here: Ääkkösiä';
var element = React.createElement(Highlight, {search: 'Aakkosia', ignoreDiacritics: true, diacriticsBlacklist: 'Ää'}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(0);

var element = React.createElement(Highlight, {search: 'Ääkkösiä', ignoreDiacritics: true, diacriticsBlacklist: 'Ää'}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(1);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('Ääkkösiä');
});

it('should support ignoring diacritics with blacklist with regex', function() {
var text = 'Letter ä is a normal letter here: Ääkkösiä';
var element = React.createElement(Highlight, {search: /k+o/i, ignoreDiacritics: true, diacriticsBlacklist: 'Ää'}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(1);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('kkö');

var element = React.createElement(Highlight, {search: /ä+/i, ignoreDiacritics: true, diacriticsBlacklist: 'Ää'}, text);
var node = TestUtils.renderIntoDocument(element);
var matches = TestUtils.scryRenderedDOMComponentsWithTag(node, 'mark');
expect(matches).to.have.length(3);
expect(ReactDOM.findDOMNode(matches[0]).textContent).to.equal('ä');
expect(ReactDOM.findDOMNode(matches[1]).textContent).to.equal('Ää');
expect(ReactDOM.findDOMNode(matches[2]).textContent).to.equal('ä');
});

it('should support escaping arbitrary string in search', function() {
var element = React.createElement(Highlight, {search: 'Test ('}, 'Test (should not throw)');
expect(TestUtils.renderIntoDocument.bind(TestUtils, element)).to.not.throw(Error);
Expand Down