Skip to content

Commit

Permalink
Add support for ignoreDiacritics/diacriticsBlacklist options
Browse files Browse the repository at this point in the history
  • Loading branch information
pasieronen committed Apr 3, 2018
1 parent 703a2c3 commit fa9392a
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 4 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ 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`
- `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
43 changes: 39 additions & 4 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) {
// 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,20 @@ 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
Expand All @@ -156,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 @@ -201,6 +234,8 @@ var Highlighter = createReactClass({

Highlighter.defaultProps = {
caseSensitive: false,
ignoreDiacritics: false,
diacriticsBlacklist: '',
matchElement: 'mark',
matchClass: 'highlight',
matchStyle: {}
Expand Down
89 changes: 89 additions & 0 deletions test/testHighlighter.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,35 @@ describe('Highlight element', function() {
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);
Expand Down Expand Up @@ -107,6 +136,66 @@ describe('Highlight element', function() {
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

0 comments on commit fa9392a

Please sign in to comment.