10
\$\begingroup\$

I am searching for some improvements, particularly in the regex, in the way I highlight specific words in a string.

  • I have keywords into my database stored without any diacritrics
  • The user comes with a string with diacritrics
  • I find the matches between the keywords and my string modified so that it does not contain any diacritrics
  • I highlight the matched words in the original sentence with diacritrics

 var defaultDiacriticsRemovalap = [
        {'base':'A', 'letters':'\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F'},
        {'base':'AA','letters':'\uA732'},
        {'base':'AE','letters':'\u00C6\u01FC\u01E2'},
        {'base':'AO','letters':'\uA734'},
        {'base':'AU','letters':'\uA736'},
        {'base':'AV','letters':'\uA738\uA73A'},
        {'base':'AY','letters':'\uA73C'},
        {'base':'B', 'letters':'\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181'},
        {'base':'C', 'letters':'\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E'},
        {'base':'D', 'letters':'\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779'},
        {'base':'DZ','letters':'\u01F1\u01C4'},
        {'base':'Dz','letters':'\u01F2\u01C5'},
        {'base':'E', 'letters':'\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E'},
        {'base':'F', 'letters':'\u0046\u24BB\uFF26\u1E1E\u0191\uA77B'},
        {'base':'G', 'letters':'\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E'},
        {'base':'H', 'letters':'\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D'},
        {'base':'I', 'letters':'\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197'},
        {'base':'J', 'letters':'\u004A\u24BF\uFF2A\u0134\u0248'},
        {'base':'K', 'letters':'\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2'},
        {'base':'L', 'letters':'\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780'},
        {'base':'LJ','letters':'\u01C7'},
        {'base':'Lj','letters':'\u01C8'},
        {'base':'M', 'letters':'\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C'},
        {'base':'N', 'letters':'\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4'},
        {'base':'NJ','letters':'\u01CA'},
        {'base':'Nj','letters':'\u01CB'},
        {'base':'O', 'letters':'\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C'},
        {'base':'OI','letters':'\u01A2'},
        {'base':'OO','letters':'\uA74E'},
        {'base':'OU','letters':'\u0222'},
        {'base':'OE','letters':'\u008C\u0152'},
        {'base':'oe','letters':'\u009C\u0153'},
        {'base':'P', 'letters':'\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754'},
        {'base':'Q', 'letters':'\u0051\u24C6\uFF31\uA756\uA758\u024A'},
        {'base':'R', 'letters':'\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782'},
        {'base':'S', 'letters':'\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784'},
        {'base':'T', 'letters':'\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786'},
        {'base':'TZ','letters':'\uA728'},
        {'base':'U', 'letters':'\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244'},
        {'base':'V', 'letters':'\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245'},
        {'base':'VY','letters':'\uA760'},
        {'base':'W', 'letters':'\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72'},
        {'base':'X', 'letters':'\u0058\u24CD\uFF38\u1E8A\u1E8C'},
        {'base':'Y', 'letters':'\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE'},
        {'base':'Z', 'letters':'\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762'},
        {'base':'a', 'letters':'\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250'},
        {'base':'aa','letters':'\uA733'},
        {'base':'ae','letters':'\u00E6\u01FD\u01E3'},
        {'base':'ao','letters':'\uA735'},
        {'base':'au','letters':'\uA737'},
        {'base':'av','letters':'\uA739\uA73B'},
        {'base':'ay','letters':'\uA73D'},
        {'base':'b', 'letters':'\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253'},
        {'base':'c', 'letters':'\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184'},
        {'base':'d', 'letters':'\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A'},
        {'base':'dz','letters':'\u01F3\u01C6'},
        {'base':'e', 'letters':'\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD'},
        {'base':'f', 'letters':'\u0066\u24D5\uFF46\u1E1F\u0192\uA77C'},
        {'base':'g', 'letters':'\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F'},
        {'base':'h', 'letters':'\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265'},
        {'base':'hv','letters':'\u0195'},
        {'base':'i', 'letters':'\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131'},
        {'base':'j', 'letters':'\u006A\u24D9\uFF4A\u0135\u01F0\u0249'},
        {'base':'k', 'letters':'\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3'},
        {'base':'l', 'letters':'\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747'},
        {'base':'lj','letters':'\u01C9'},
        {'base':'m', 'letters':'\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F'},
        {'base':'n', 'letters':'\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5'},
        {'base':'nj','letters':'\u01CC'},
        {'base':'o', 'letters':'\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275'},
        {'base':'oi','letters':'\u01A3'},
        {'base':'ou','letters':'\u0223'},
        {'base':'oo','letters':'\uA74F'},
        {'base':'p','letters':'\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755'},
        {'base':'q','letters':'\u0071\u24E0\uFF51\u024B\uA757\uA759'},
        {'base':'r','letters':'\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783'},
        {'base':'s','letters':'\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B'},
        {'base':'t','letters':'\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787'},
        {'base':'tz','letters':'\uA729'},
        {'base':'u','letters': '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289'},
        {'base':'v','letters':'\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C'},
        {'base':'vy','letters':'\uA761'},
        {'base':'w','letters':'\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73'},
        {'base':'x','letters':'\u0078\u24E7\uFF58\u1E8B\u1E8D'},
        {'base':'y','letters':'\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF'},
        {'base':'z','letters':'\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763'}
    ];

    var diacriticsMap = {};
    for (var i=0; i < defaultDiacriticsRemovalap.length; i++){
        var letters = defaultDiacriticsRemovalap[i].letters;
        for (var j=0; j < letters.length ; j++){
            diacriticsMap[letters[j]] = defaultDiacriticsRemovalap[i].base;
        }
    }

    // "what?" version ... http://jsperf.com/diacritics/12
    function removeDiacritics (str) {
        return str.replace(/[^\u0000-\u007E]/g, function(a){ 
           return diacriticsMap[a] || a; 
        });
    }

//Function to highlight matched keywords :
var sentence = "J'eusse été amélioré sans des aigÜs si hauts améliorés et raisonnable. Amélioration réaméliorée";
var output = sentence;
var word = ['eusse','ameliore','aigu','aimerais']; //Matched keywords extracted from database where there are stored without diatrics

var new_s = sentence, match, output, nb = 0, index = [];
var new_s_cache = removeDiacritics(sentence).replace("'"," ");
output = output + "<br />" + new_s_cache ;

for (var i=0;i<word.length;i++){// For each matched word it stores its position and length in index
  var wordInput = word[i].substr(0,4);
  var re = new RegExp("(?:^|\\W)"+wordInput+"(\\w*)(?!\\w)","gi");
  while (match = re.exec(new_s_cache)) {
    index.push([match.index,match[0].length]);
  }
}
index.sort(); // Sort index so that it highlights word in the right order
for (var j=0;j<index.length;j++) { //It hightlights the word in the orginal sentence with diatrics
    new_s = new_s.substr(0,index[j][0]+1+nb) + "<b>" + new_s.substr(index[j][0]+1+nb,index[j][1]) + "</b>" + new_s.substr(index[j][0]+index[j][1]+1+nb);
    nb = nb + 7; // take into account the "<b></b>" length
}

output = output + "<br />" + new_s ;

document.getElementById('test').innerHTML = output;

Where I am seeking improvements:

  • The regex: I want to catch the similar words to keywords. That's why I took arbitrary the 4 first letters and then run a regex to find the words that contains this 4 letters but it's not optimal. Indeed for instance I'd like to find 'unapproved' when the keyword is 'approve'.
  • The idea of the code: the juggle between the original sentence and the one without diacritrics appears to be quite fragile.

jsFiddle

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Is the normalization of the text to diacritic-free form important to you, or is it merely an intermediate step towards the goal of finding the search terms? \$\endgroup\$ Commented Feb 3, 2016 at 19:40
  • \$\begingroup\$ Could you explain why "Amélioration" is highlighted in your example, when the search term is "ameliore"? Is word stemming part of the exercise, or is that a bug? \$\endgroup\$ Commented Feb 3, 2016 at 19:41
  • 1
    \$\begingroup\$ @200_success Yes the normalization is important to catch words like "côté" which can be badly written "coté" for instance. And secondly yes the word stemming is a part of the exercise : for instance I want to be able to catch amélioration amélioré and reamélioré with the same keyword amélioré reduced here to its four first letters 'amel' \$\endgroup\$
    – fazac
    Commented Feb 3, 2016 at 22:13
  • 1
    \$\begingroup\$ Please clarify the correct stemming behaviour. In your example, "ameliore" matches "Amélioration" (which I don't expect to match), and fails to match "réaméliorée" (which I do expect to match). \$\endgroup\$ Commented Feb 3, 2016 at 22:18
  • \$\begingroup\$ All right, that is a part of improvements I am searching for. I would like to be able to catch any word similar to keywords (which could be anything with at least four letters but not a stop word). And 'similar' is to define. Here I am telling that all the words with the same four first letters as the keywords' are similar. I know it is not perfect and I think that the regex could be improve so that it catches the xx+fourFirstLetterOf(keywords) words for instance. \$\endgroup\$
    – fazac
    Commented Feb 3, 2016 at 22:25

1 Answer 1

2
\$\begingroup\$

You can avoid messing around with string indexes if you don't try to normalize the text. Rather, you can transform the search terms into diacritic-insensitive regular expressions. For example, "eusse" could become /(?:e|é|É|è|È|ë|Ë)(?:u|ü|Ü)ss(?:e|é|É|è|È|ë|Ë)/. You can then apply that regular expression on the text directly.

function normalizedRegExpMaker(normalizationMap) {
    // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
    function escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    // Invert the map, so that the values become keys, and the keys are listed
    // as values.
    var diacriticReverse = (function(equivs) {
        var reverse = {};
        for (c in equivs) {
            if (!reverse[equivs[c]]) {
                reverse[equivs[c]] = [equivs[c]];
            }
            reverse[equivs[c]].push(c);
        }
        return reverse;
    })(normalizationMap);

    var diacriticReverseRe = new RegExp(
        '(?:' + Object.keys(diacriticReverse).join('|') + ')',
        'g'
    );

    return function(s) {
        return escapeRegExp(s).replace(diacriticReverseRe, function(c) {
            return '(?:' + diacriticReverse[c].join('|') + ')';
        });
    };
}

function normalizedWordReplace(normalizationMap, words, text, transform) {
    var lettersRe = '[\\w' + Object.keys(normalizationMap).join('') + ']*';
    var regexpMaker = normalizedRegExpMaker(normalizationMap);
    var re = new RegExp(
        lettersRe + '(?:' + words.map(regexpMaker).join('|') + ')' + lettersRe,
        'gi'
    );

    return text.replace(re, transform);
}

function bolden(s) {
    return '<b>' + s + '</b>';
}

var FRENCH_NORMALIZATION = {
    'ç': 'c',      'Ç': 'c',
    'à': 'a',      'À': 'a',
    'é': 'e',      'É': 'e',
    'è': 'e',      'È': 'e',
    'ë': 'e',      'Ë': 'e',
    'æ': 'ae',     'Æ': 'ae',
    'œ': 'oe',     'Œ': 'oe',
    'ü': 'u',      'Ü': 'u',
};

function search() {
    var searchTerms = $('#terms').val().split(/\s+/);
    var text = $('#text').val();
    var output = normalizedWordReplace(FRENCH_NORMALIZATION, searchTerms, text, bolden);
    $('#output').html(output);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<form>
  <label>Search terms (space-separated):<br>
    <input id="terms" size="80"
           value="eusse ameliore aigu aimerais">
  </label>
  <br>
  <label>Text:<br>
    <input id="text" size="80"
           value="J'eusse été amélioré sans des aigÜs si hauts améliorés et raisonnable. Amélioration réaméliorée">
  </label>
  <br>
  <button onclick="search()">Search</button>
</form>
<div id="output"></div>

Note that in this demo, as in yours, I am playing fast and loose with HTML escaping. If the text contains, say, a <script> tag, it will be injected into the resulting display.

You appear to have used an all-purpose normalization map. Note that there are likely language-specific rules. French treats diacritics as mere decorations. However, that isn't the case in German, where 'ü' is the common way of writing "ue".

\$\endgroup\$
3
  • \$\begingroup\$ What a brilliant idea! And really fast answer! I reassure you : Nope the text will not contain html tags. And yes, I'll have to be careful about the used language indeed, I had not seen that point. I'll run all that tomorrow but before, could you describe what you're doing when you're typing "var lettersRe = '[\\w' + Object.keys(normalizationMap).join('') + ']*';" please? \$\endgroup\$
    – fazac
    Commented Feb 3, 2016 at 23:19
  • \$\begingroup\$ I'd like a regex that includes all letters. Unfortunately, /\w/ in JavaScript is equivalent to /[A-Za-z0-9_]/, which covers just the plain Roman letters. Therefore, I have to supplement it with the recognized diacritic-decorated characters (which are the keys of normalizationMap). \$\endgroup\$ Commented Feb 3, 2016 at 23:23
  • \$\begingroup\$ Wow I'm listening new concepts everyday, diacriticReverse \$\endgroup\$ Commented Oct 15, 2018 at 7:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.