From 0907f4aadf08f4d90730fb37ecc313670f1ad561 Mon Sep 17 00:00:00 2001 From: Pasi Eronen Date: Tue, 3 Apr 2018 10:48:23 +0300 Subject: [PATCH 1/3] Use HTML tag by default instead of --- README.md | 2 +- lib/highlighter.js | 2 +- test/testHighlighter.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 39f5d08..fd0f590 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ 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` +- `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. diff --git a/lib/highlighter.js b/lib/highlighter.js index c5e5bec..d671194 100644 --- a/lib/highlighter.js +++ b/lib/highlighter.js @@ -196,7 +196,7 @@ var Highlighter = createReactClass({ Highlighter.defaultProps = { caseSensitive: false, - matchElement: 'strong', + matchElement: 'mark', matchClass: 'highlight', matchStyle: {} }; diff --git a/test/testHighlighter.js b/test/testHighlighter.js index e79397f..3dc757a 100644 --- a/test/testHighlighter.js +++ b/test/testHighlighter.js @@ -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; @@ -57,14 +57,14 @@ 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') @@ -73,7 +73,7 @@ describe('Highlight element', function() { 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'); From 703a2c34166863b740db3ef4b822d4402fe56932 Mon Sep 17 00:00:00 2001 From: Pasi Eronen Date: Tue, 3 Apr 2018 11:10:57 +0300 Subject: [PATCH 2/3] Avoid infinite loop for zero-width regex matches --- lib/highlighter.js | 5 +++++ test/testHighlighter.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/lib/highlighter.js b/lib/highlighter.js index d671194..03ee0fa 100644 --- a/lib/highlighter.js +++ b/lib/highlighter.js @@ -142,6 +142,11 @@ var Highlighter = createReactClass({ var boundaries = this.getMatchBoundaries(remaining, 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); if (nonMatch) { diff --git a/test/testHighlighter.js b/test/testHighlighter.js index 3dc757a..a90468f 100644 --- a/test/testHighlighter.js +++ b/test/testHighlighter.js @@ -70,6 +70,14 @@ describe('Highlight element', function() { 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 regular expressions in search', function() { var element = React.createElement(Highlight, {search: /[A-Za-z]+/}, 'Easy as 123, ABC...'); var node = TestUtils.renderIntoDocument(element); @@ -79,6 +87,26 @@ describe('Highlight element', function() { 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 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); From fa9392a55bacb418ef1b5327dba922f3829941ca Mon Sep 17 00:00:00 2001 From: Pasi Eronen Date: Tue, 3 Apr 2018 11:11:27 +0300 Subject: [PATCH 3/3] Add support for ignoreDiacritics/diacriticsBlacklist options --- README.md | 2 + lib/highlighter.js | 43 ++++++++++++++++++-- test/testHighlighter.js | 89 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fd0f590..23ffe9e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/highlighter.js b/lib/highlighter.js index 03ee0fa..cc4d3aa 100644 --- a/lib/highlighter.js +++ b/lib/highlighter.js @@ -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, @@ -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 @@ -26,6 +49,8 @@ var Highlighter = createReactClass({ this.props, 'search', 'caseSensitive', + 'ignoreDiacritics', + 'diacriticsBlacklist', 'matchElement', 'matchClass', 'matchStyle' @@ -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); }, @@ -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 @@ -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. @@ -201,6 +234,8 @@ var Highlighter = createReactClass({ Highlighter.defaultProps = { caseSensitive: false, + ignoreDiacritics: false, + diacriticsBlacklist: '', matchElement: 'mark', matchClass: 'highlight', matchStyle: {} diff --git a/test/testHighlighter.js b/test/testHighlighter.js index a90468f..cec213b 100644 --- a/test/testHighlighter.js +++ b/test/testHighlighter.js @@ -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); @@ -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);