Thursday, September 20, 2018

C# / ASP.NET TinyMCE spell checker handler integration

All day I was looking for simple solution to integrate TinyMCE spell checker option (the free version of the spell checker) into a project I am currently working. Unfortunately I could not find any alternative for the server side of the spell checker with permissive license terms. So I decided to do one on my own. Which is a way much simpler to implement than any of the others commercial server side spell checkers. So here it is:

Prerequisites:

Install from Nuget NHunspell package so you would be able to use the dictionary files .aff and .dic


Usings:

using NHunspell;
using System.IO;
using System;
using System.Web;
using System.Text;
using System.Collections.Generic;


Simple ashx handler source:

   public void ProcessRequest (HttpContext context) {
        var Language = context.Request.Form["lang"];
        var Words = context.Request.Form["text"];
        var Action = context.Request.Form["method"];
        string JSON = "{\"words\": [{}]}";
        List<string> WordsList = new List<string>();
        List<Tuple<int, string>> WordsListLevenshtein = new List<Tuple<int, string>>();

        if (Language != null && Words != null && Action != null && File.Exists(context.Server.MapPath("~/Upload/Dictionaries/" + Language + ".aff")) && File.Exists(context.Server.MapPath("~/Upload/Dictionaries/" + Language + ".dic")))
        {
            Language = Functions.StripHTML(Language);
            Words = Functions.StripHTML(Words);
            Action = Functions.StripHTML(Action);

            if (Action.ToString().ToLower() == "spellcheck" && Words.Trim() != String.Empty)
            {
                JSON = "{\"words\": {";

                foreach(string Word in Words.Split(new char[0]))
                {
                    try
                    {
                        Hunspell hunspell = new Hunspell(context.Server.MapPath("~/Upload/Dictionaries/" + Language + ".aff"), context.Server.MapPath("~/Upload/Dictionaries/" + Language + ".dic"));
                     
                        if(!hunspell.Spell(Word.Trim()))
                        {
                            List<string> suggestions = hunspell.Suggest(Word.Trim());

                            if (suggestions.Count > 0)
                            {
                                foreach (string suggestion in suggestions)
                                {
                                    if (!WordsList.Contains(suggestion))
                                    {
                                        WordsList.Add(suggestion);
                                    }
                                }

                                foreach (string item in WordsList)
                                {
                                    WordsListLevenshtein.Add(new Tuple<int, string>(Functions.LevenshteinDistance(Word.Trim(), item), item));
                                }

                                WordsList.Clear();

                                WordsListLevenshtein.Sort((a, b) => b.Item1.CompareTo(a.Item1));

                                if (WordsListLevenshtein.Count > 10)
                                {
                                    WordsListLevenshtein.RemoveRange(10, WordsListLevenshtein.Count - 10);
                                }

                                JSON += "\"" + Word.Trim() + "\": [";

                                foreach (var item in WordsListLevenshtein)
                                {
                                    JSON += "\"" + item.Item2 + "\", ";
                                }

                                WordsListLevenshtein.Clear();

                                int index = JSON.LastIndexOf(',');
                                JSON = JSON.Remove(index, 1) + "], ";
                            }
                        }

                        hunspell.Dispose();
                    }
                    catch (Exception ex)
                    {
                        JSON = "{\"words\": [{\"error\": \"" + HttpUtility.JavaScriptStringEncode(ex.Message) + "\"}]}";
                    }
                }

                int lastcomma = JSON.LastIndexOf(',');

                if (lastcomma > -1)
                {
                   JSON = JSON.Remove(lastcomma, 1) + "}}";
                }
                else
                {
                   JSON = JSON + "}}";
                }
            }
        }

        context.Response.ContentEncoding = Encoding.UTF8;
        context.Response.Cache.SetExpires(DateTime.UtcNow.AddYears(-1));
        context.Response.Headers.Add("Cache-Control", "no-store, no-cache, must-revalidate");
        context.Response.Headers.Add("Pragma", "no-cache");
        context.Response.ContentType = "application/json";
        context.Response.Write(JSON);
    }


A bit of extra push and implementing Levenshtein word distance just to make sure the suggested words are ordered in the most closest first in the list.


  public static int LevenshteinDistance(string SearchString, string FoundString)
        {
            var source1Length = SearchString.Length;
            var source2Length = FoundString.Length;
            var matrix = new int[source1Length + 1, source2Length + 1];

            if (source1Length == 0)
            {
                return source2Length;
            }

            if (source2Length == 0)
            {
                return source1Length;
            }

            for (var i = 0; i <= source1Length; matrix[i, 0] = i++) { }
            for (var j = 0; j <= source2Length; matrix[0, j] = j++) { }
            for (var i = 1; i <= source1Length; i++)
            {
                for (var j = 1; j <= source2Length; j++)
                {
                    var cost = (FoundString[j - 1] == SearchString[i - 1]) ? 0 : 1;
                    matrix[i, j] = Math.Min(Math.Min(matrix[i - 1, j] + 1, matrix[i, j - 1] + 1), matrix[i - 1, j - 1] + cost);
                }
            }

            return matrix[source1Length, source2Length];

        }



And last but not least the TinyMCE init:

tinymce.init({
            selector: ".tinymce",
            plugins: ["advlist autolink lists link image charmap preview searchreplace code fullscreen   textcolor colorpicker insertdatetime table contextmenu paste textcolor nonbreaking media emoticons   importcss hr pagebreak anchor wordcount print code visualblocks visualchars spellchecker "],
        toolbar1: " spellchecker | print paste | fontselect | fontsizeselect | bold italic underline | undo redo  | styleselect | alignleft aligncenter alignright alignjustify  | forecolor  backcolor | code preview  fullscreen ",
            spellchecker_rpc_url: '<%=ResolveUrl("~/Handlers/SpellChecker.ashx") %>',
            spellchecker_languages: "Afrikaans=af,አማርኛ=am,Български=bg,Deutsch=de,Español=es,Français=fr,Македонски=mk,Românește=ro,Русский=ru,Português=pt,српски=sr,Українська мова=uk,Türkçe=tr,Polski=pl,हिन्दी=hi,ελληνικά=el,Kiswahili=sw,汉语=zh,Melayu=ms,Italiano=it,English=en",
})


Oh and one more last thing you will need your dictionaries Hunspell format .aff and .dic which you can get from the repository of the Firefox browser. And here is how to...

1. Open you Firefox browser and go to https://addons.mozilla.org/en-US/firefox/search/?platform=windows&q=spellcheck

2. Find your desired language spellcheck dictionary (make sure it is spell check dictionary not Firefox interface dictionary!).

3. Install the addon for the selected spell checking dictionary.

4. Go to %AppData%      Roaming\Mozilla\Firefox\Profiles your profile folder  than extensions folder and then you will see the dictionary file installed there (all dictionary files are *.xpi extension).

5. Copy the desired dictionary file from the profiles folder to another location (your desktop folder) and replace the extension xpi with zip and then unzip the file.

6. In the unzipped folder you will find subfolder called dictionaries which contains the *.aff and *.dic files, copy them in your projects (Upload/Dictionaries in my case) folder.

You are good to go from here on.

Let me know if it helped you or you have any suggestions.




No comments:

Post a Comment