Index: CHANGELOG.txt
==================================================================
--- CHANGELOG.txt
+++ CHANGELOG.txt
@@ -22,16 +22,16 @@
     Lexicographer
     French Conjugueur
 
 ##  0.4
     Suggestion mechanisms
-    Simplier user options writing
+    Simpler user options writing
     Application Launcher
     Author field edition
 
 ##  0.5
     Grammalecte is an autonomous package, free from Hunspell and LibreOffice
     Indexable binary dictionary (DAWG-FSA) generator
     Disambiguator
     Multi-actions rules (bi-passes engine again)
-    Simplier options for word boundaries
+    Simpler options for word boundaries
     Unit tests

Index: LICENSE.fr.txt
==================================================================
--- LICENSE.fr.txt
+++ LICENSE.fr.txt
@@ -113,11 +113,11 @@
 vous assurer qu’eux aussi reçoivent ou peuvent recevoir son code
 source. Et vous devez leur montrer les termes de cette licence afin
 qu’ils connaissent leurs droits.
 
 Les développeurs qui utilisent la GPL GNU protègent vos droits en deux
-étapes : (1) ils affirment leur droits d’auteur (“copyright”) sur le
+étapes : (1) ils affirment leurs droits d’auteur (“copyright”) sur le
 logiciel, et (2) vous accordent cette Licence qui vous donne la
 permission légale de le copier, le distribuer et/ou le modifier.
 
 Pour la protection des développeurs et auteurs, la GPL stipule
 clairement qu’il n’y a pas de garantie pour ce logiciel libre. Aux fins
@@ -130,11 +130,11 @@
 à l’installation ou l’exécution de versions modifiées du logiciel à
 l’intérieur de ces dispositifs, alors que les fabricants le peuvent.
 Ceci est fondamentalement incompatible avec le but de protéger la
 liberté des utilisateurs de modifier le logiciel. L’aspect systématique
 de tels abus se produit dans le secteur des produits destinés aux
-utilisateurs individuels, ce qui est précidément ce qui est le plus
+utilisateurs individuels, ce qui est précisément ce qui est le plus
 inacceptable. Aussi, nous avons conçu cette version de la GPL pour
 prohiber cette pratique pour ces produits. Si de tels problèmes
 surviennent dans d’autres domaines, nous nous tenons prêt à étendre
 cette restriction à ces domaines dans de futures versions de la GPL,
 autant qu’il sera nécessaire pour protéger la liberté des utilisateurs.
@@ -357,16 +357,16 @@
 
 Une compilation d’un Travail Couvert avec d’autres travaux séparés et
 indépendants, qui ne sont pas par leur nature des extensions du Travail
 Couvert, et qui ne sont pas combinés avec lui de façon à former un
 programme plus large, dans ou sur un volume de stockage ou un support
-de distribution, est appelé un « aggrégat » si la compilation et son
+de distribution, est appelé un « agrégat » si la compilation et son
 Droit d’Auteur résultant ne sont pas utilisés pour limiter l’accès ou
-les droits légaux des utilisateurs de la compilation en deça de ce que
+les droits légaux des utilisateurs de la compilation en deçà de ce que
 permettent les travaux individuels. L’inclusion d’un Travail Couvert
-dans un aggrégat ne cause pas l’application de cette Licence aux
-autres parties de l’aggrégat.
+dans un agrégat ne cause pas l’application de cette Licence aux
+autres parties de l’agrégat.
 
 
 Article 6. Acheminement des formes non sources.
 
 Vous pouvez acheminer sous forme de code objet un Travail Couvert
@@ -392,11 +392,11 @@
      de la source, ou soit (2) un accès permettant de copier le Source
      Correspondant depuis un serveur réseau sans frais.
 
   c) Acheminer des copies individuelles du code objet avec une copie de
      l’offre écrite de fournir le Source Correspondant. Cette
-     alternative est permise seulement occasionellement et non
+     alternative est permise seulement occasionnellement et non
      commercialement, et seulement si vous avez reçu le code objet avec
      une telle offre, en accord avec l’article 6 alinéa b.
 
   d) Acheminer le code objet en offrant un accès depuis un emplacement
      désigné (gratuit ou contre facturation) et offrir un accès
@@ -418,11 +418,11 @@
      d’égal-à-égal, pourvu que vous informiez les autres participants
      sur où le code objet et le Source Correspondant du travail sont
      offerts sans frais au public général suivant l’article 6 alinéa d.
      Une portion séparable du code objet, dont le code source est exclu
      du Source Correspondant en tant que Bibliothèque Système, n’a pas
-     besoin d’être inclu dans l’acheminement du travail sous forme de
+     besoin d’être inclus dans l’acheminement du travail sous forme de
      code objet.
 
 Un « Produit Utilisateur » est soit (1) un « Produit de Consommation, »
 ce qui signifie toute propriété personnelle tangible normalement
 utilisée à des fins personnelles, familiales ou relatives au foyer,
@@ -457,11 +457,11 @@
 Utilisateur est transféré au Destinataire définitivement ou pour un
 terme fixé (indépendamment de la façon dont la transaction est
 caractérisée), le Source Correspondant acheminé selon cet article-ci
 doit être accompagné des Informations d’Installation. Mais cette
 obligation ne s’applique pas si ni vous ni aucune tierce partie ne
-détient la possibilité d’intaller un code objet modifié sur le Produit
+détient la possibilité d’installer un code objet modifié sur le Produit
 Utilisateur (par exemple, le travail a été installé en mémoire morte).
 
 L’obligation de fournir les Informations d’Installation n’inclue pas
 celle de continuer à fournir un service de support, une garantie ou des
 mises à jour pour un travail qui a été modifié ou installé par le
@@ -479,23 +479,23 @@
 la copie.
 
 
 Article 7. Termes additionnels.
 
-Les « permissions additionelles » désignent les termes qui
+Les « permissions additionnelles » désignent les termes qui
 supplémentent ceux de cette Licence en émettant des exceptions à l’une
 ou plusieurs de ses conditions. Les permissions additionnelles qui
 sont applicables au Programme entier doivent être traitées comme si
-elles étaient incluent dans cette Licence, dans les limites de leur
+elles étaient incluses dans cette Licence, dans les limites de leur
 validité suivant la loi applicable. Si des permissions additionnelles
 s’appliquent seulement à une partie du Programme, cette partie peut
 être utilisée séparément suivant ces permissions, mais le Programme
 tout entier reste gouverné par cette Licence sans regard aux
-permissions additionelles.
+permissions additionnelles.
 
 Quand vous acheminez une copie d’un Travail Couvert, vous pouvez à
-votre convenance ôter toute permission additionelle de cette copie, ou
+votre convenance ôter toute permission additionnelle de cette copie, ou
 de n’importe quelle partie de celui-ci. (Des permissions
 additionnelles peuvent être rédigées de façon à requérir leur propre
 suppression dans certains cas où vous modifiez le travail.) Vous
 pouvez placer les permissions additionnelles sur le matériel acheminé,
 ajoutées par vous à un Travail Couvert pour lequel vous avez ou pouvez
@@ -664,11 +664,11 @@
 à cette partie.
 
 Si vous acheminez un Travail Couvert, dépendant en connaissance d’une
 licence de brevet, et si le Source Correspondant du travail n’est pas
 disponible à quiconque copie, sans frais et suivant les termes de cette
-Licence, à travers un serveur réseau publiquement acessible ou tout
+Licence, à travers un serveur réseau publiquement accessible ou tout
 autre moyen immédiatement accessible, alors vous devez soit (1) rendre
 la Source Correspondante ainsi disponible, soit (2) vous engager à vous
 priver pour vous-même du bénéfice de la licence de brevet pour ce
 travail particulier, soit (3) vous engager, d’une façon consistante
 avec les obligations de cette Licence, à étendre la licence de brevet
@@ -714,11 +714,11 @@
 
 Si des conditions vous sont imposées (que ce soit par décision
 judiciaire, par un accord ou autrement) qui contredisent les conditions
 de cette Licence, elles ne vous excusent pas des conditions de cette
 Licence. Si vous ne pouvez pas acheminer un Travail Couvert de façon à
-satisfaire simulténément vos obligations suivant cette Licence et
+satisfaire simultanément vos obligations suivant cette Licence et
 toutes autres obligations pertinentes, alors en conséquence vous ne
 pouvez pas du tout l’acheminer. Par exemple, si vous avez un accord sur
 des termes qui vous obligent à collecter pour le réacheminement des
 royalties depuis ceux à qui vous acheminez le Programme, la seule façon
 qui puisse vous permettre de satisfaire à la fois à ces termes et ceux
@@ -762,12 +762,12 @@
 versions futures de la Licence Générale Publique GNU peut être
 utilisée, la déclaration publique d’acceptation d’une version par cet
 intermédiaire vous autorise à choisir cette version pour le Programme.
 
 Des versions ultérieures de la licence peuvent vous donner des
-permissions additionelles ou différentes. Cependant aucune obligation
-additionelle n’est imposée à l’un des auteurs ou titulaires de Droit
+permissions additionnelles ou différentes. Cependant aucune obligation
+additionnelle n’est imposée à l’un des auteurs ou titulaires de Droit
 d’Auteur du fait de votre choix de suivre une version ultérieure.
 
 
 Article 15. Déclaration d’absence de garantie.
 

Index: THANKS.txt
==================================================================
--- THANKS.txt
+++ THANKS.txt
@@ -1,11 +1,11 @@
 # THANKS
 
 ## Thanks to all who contributed to the project
 
 László Németh (creator of Lightproof, from which Grammalecte forked),
-Dominique Pelé (dominiko),
+Dominique Pellé (dominiko),
 Jean-Luc T. (Tbj),
 Pierre Choffardet (pitpit),
 
 
 ## Thanks to all those who supported us 
@@ -179,6 +179,7 @@
 Yann Asset,
 Yann Brelière,
 Yannick Geynet,
 Yelin
 
-et aux centaines de contributeurs qui ont préféré garder l’anonymat, ainsi qu’à la GPL3 de Richard Matthew Stallman.
+and to hundred of supporters who prefered to keep their anonymity,
+and to the GPL3 by Richard Matthew Stallman.

Index: compile_rules.py
==================================================================
--- compile_rules.py
+++ compile_rules.py
@@ -449,11 +449,11 @@
     del lRule[-1] # tGroups positioning codes are useless for Python
     # error messages
     for aAction in lRuleJS[6]:
         if aAction[1] == "-":
             aAction[2] = aAction[2].replace(" ", " ") # nbsp --> nnbsp
-            aAction[4] = aAction[4].replace("« ", "« ").replace(" »", " »")
+            aAction[4] = aAction[4].replace("« ", "« ").replace(" »", " »").replace(" :", " :").replace(" :", " :")
     # js regexes
     lRuleJS[1], lNegLookBehindRegex = regex2js( dJSREGEXES.get(lRuleJS[3], lRuleJS[1]) )
     lRuleJS.append(lNegLookBehindRegex)
     return lRuleJS
 

Index: gc_core/js/helpers.js
==================================================================
--- gc_core/js/helpers.js
+++ gc_core/js/helpers.js
@@ -30,10 +30,19 @@
     } else {
         console.error(sMsg);
     }
 }
 
+function inspect (o) {
+    let sMsg = "__inspect__: " + typeof o;
+    for (let sParam in o) {
+        sMsg += "\n" + sParam + ": " + o.sParam;
+    }
+    sMsg += "\n" + JSON.stringify(o) + "\n__end__";
+    echo(sMsg);
+}
+
 
 // load ressources in workers (suggested by Mozilla extensions reviewers)
 // for more options have a look here: https://gist.github.com/Noitidart/ec1e6b9a593ec7e3efed
 // if not in workers, use sdk/data.load() instead
 function loadFile (spf) {
@@ -78,11 +87,12 @@
         obj[k] = v;
     }
     return obj;
 }
 
+exports.setLogOutput = setLogOutput;
 exports.echo = echo;
 exports.logerror = logerror;
+exports.inspect = inspect;
 exports.objectToMap = objectToMap;
 exports.mapToObject = mapToObject;
-exports.setLogOutput = setLogOutput;
 exports.loadFile = loadFile;

Index: gc_core/js/text.js
==================================================================
--- gc_core/js/text.js
+++ gc_core/js/text.js
@@ -37,19 +37,20 @@
 }
 
 function getReadableError (oErr) {
     // Returns an error oErr as a readable error
     try {
-        let s = "\n* " + oErr['nStart'] + ":" + oErr['nEnd'] + "  # " + oErr['sRuleId']+":\n";
-        s += "  " + oErr["sMessage"];
+        let sResult = "\n* " + oErr['nStart'] + ":" + oErr['nEnd'] 
+                    + "  # " + oErr['sLineId'] + "  # " + oErr['sRuleId'] + ":\n";
+        sResult += "  " + oErr["sMessage"];
         if (oErr["aSuggestions"].length > 0) {
-            s += "\n  > Suggestions : " + oErr["aSuggestions"].join(" | ");
+            sResult += "\n  > Suggestions : " + oErr["aSuggestions"].join(" | ");
         }
         if (oErr["URL"] !== "") {
-            s += "\n  > URL: " + oErr["URL"];
+            sResult += "\n  > URL: " + oErr["URL"];
         }
-        return s;
+        return sResult;
     }
     catch (e) {
         helpers.logerror(e);
         return "\n# Error. Data: " + oErr.toString();
     }

Index: gc_core/js/tokenizer.js
==================================================================
--- gc_core/js/tokenizer.js
+++ gc_core/js/tokenizer.js
@@ -3,17 +3,16 @@
 
 "use strict";
 
 const helpers = require("resource://grammalecte/helpers.js");
 
-
 const aPatterns = {
     // All regexps must start with ^.
     "default":
         [
             [/^[   \t]+/, 'SPACE'],
-            [/^[,.;:!?…«»“”"()/·]+/, 'SEPARATOR'],
+            [/^[,.;:!?…«»“”‘’"(){}\[\]/·–—]+/, 'SEPARATOR'],
             [/^(?:https?:\/\/|www[.]|[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st_]+[@.][a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st_]+[@.])[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st_.\/?&!%=+*"'@$#-]+/, 'LINK'],
             [/^[#@][a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st_-]+/, 'TAG'],
             [/^<[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+.*?>|<\/[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+ *>/, 'HTML'],
             [/^\[\/?[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+\]/, 'PSEUDOHTML'],
             [/^&\w+;(?:\w+;|)/, 'HTMLENTITY'],
@@ -22,19 +21,19 @@
             [/^[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+(?:[’'`-][a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+)*/, 'WORD']
         ],
     "fr":
         [
             [/^[   \t]+/, 'SPACE'],
-            [/^[,.;:!?…«»“”"()/·]+/, 'SEPARATOR'],
+            [/^[,.;:!?…«»“”‘’"(){}\[\]/·–—]+/, 'SEPARATOR'],
             [/^(?:https?:\/\/|www[.]|[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st_]+[@.][a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st_]+[@.])[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st_.\/?&!%=+*"'@$#-]+/, 'LINK'],
             [/^[#@][a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st_-]+/, 'TAG'],
             [/^<[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+.*?>|<\/[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+ *>/, 'HTML'],
             [/^\[\/?[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+\]/, 'PSEUDOHTML'],
             [/^&\w+;(?:\w+;|)/, 'HTMLENTITY'],
             [/^(?:l|d|n|m|t|s|j|c|ç|lorsqu|puisqu|jusqu|quoiqu|qu)['’`]/i, 'ELPFX'],
             [/^\d\d?[hm]\d\d\b/, 'HOUR'],
-            [/^\d+(?:er|nd|e|de|ième|ème|eme)\b/, 'ORDINAL'],
+            [/^\d+(?:er|nd|e|de|ième|ème|eme)s?\b/, 'ORDINAL'],
             [/^-?\d+(?:[.,]\d+|)/, 'NUM'],
             [/^[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+(?:[’'`-][a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+)*/, 'WORD']
         ]
 }
 
@@ -44,11 +43,11 @@
     constructor (sLang) {
         this.sLang = sLang;
         if (!aPatterns.hasOwnProperty(sLang)) {
             this.sLang = "default";
         }
-        this.aRules = aPatterns[sLang];
+        this.aRules = aPatterns[this.sLang];
     };
 
     * genTokens (sText) {
         let m;
         let i = 0;
@@ -55,11 +54,17 @@
         while (sText) {
             let nCut = 1;
             for (let [zRegex, sType] of this.aRules) {
                 try {
                     if ((m = zRegex.exec(sText)) !== null) {
-                        yield { "sType": sType, "sValue": m[0], "nStart": i, "nEnd": i + m[0].length }
+                        if (sType == 'SEPARATOR') {
+                            for (let c of m[0]) {
+                                yield { "sType": sType, "sValue": c, "nStart": i, "nEnd": i + m[0].length }    
+                            }
+                        } else {
+                            yield { "sType": sType, "sValue": m[0], "nStart": i, "nEnd": i + m[0].length }    
+                        }
                         nCut = m[0].length;
                         break;
                     }
                 }
                 catch (e) {
@@ -67,9 +72,19 @@
                 }
             }
             i += nCut;
             sText = sText.slice(nCut);
         }
+    };
+
+    getSpellingErrors (sText, oDict) {
+        let aSpellErr = [];
+        for (let oToken of this.genTokens(sText)) {
+            if (oToken.sType === 'WORD' && !oDict.isValidToken(oToken.sValue)) {
+                aSpellErr.push(oToken);
+            }
+        }
+        return aSpellErr;
     }
 }
 
 exports.Tokenizer = Tokenizer;

Index: gc_lang/fr/config.ini
==================================================================
--- gc_lang/fr/config.ini
+++ gc_lang/fr/config.ini
@@ -3,11 +3,11 @@
 lang_name = French
 locales = fr_FR fr_BE fr_CA fr_CH fr_LU fr_MC fr_BF fr_CI fr_SN fr_ML fr_NE fr_TG fr_BJ
 country_default = FR
 name = Grammalecte
 implname = grammalecte
-version = 0.5.17.2
+version = 0.5.18
 author = Olivier R.
 provider = Dicollecte
 link = http://grammalecte.net
 description = Correcteur grammatical pour le français.
 extras = README_fr.txt

Index: gc_lang/fr/modules-js/lexicographe.js
==================================================================
--- gc_lang/fr/modules-js/lexicographe.js
+++ gc_lang/fr/modules-js/lexicographe.js
@@ -2,13 +2,15 @@
 // License: MPL 2
 
 "use strict";
 
 ${string}
+${map}
 
 
 const helpers = require("resource://grammalecte/helpers.js");
+const tkz = require("resource://grammalecte/tokenizer.js");
 
 
 const _dTAGS = new Map ([
     [':G', "[mot grammatical]"],
     [':N', " nom,"],
@@ -155,10 +157,42 @@
     ["m'en", " (me) pronom personnel objet + (en) pronom adverbial"],
     ["t'en", " (te) pronom personnel objet + (en) pronom adverbial"],
     ["s'en", " (se) pronom personnel objet + (en) pronom adverbial"]
 ]);
 
+const _dSeparator = new Map ([
+    ['.', "point"],
+    ['·', "point médian"],
+    ['…', "points de suspension"],
+    [':', "deux-points"],
+    [';', "point-virgule"],
+    [',', "virgule"],
+    ['?', "point d’interrogation"],
+    ['!', "point d’exclamation"],
+    ['(', "parenthèse ouvrante"],
+    [')', "parenthèse fermante"],
+    ['[', "crochet ouvrante"],
+    [']', "crochet fermante"],
+    ['{', "accolade ouvrante"],
+    ['}', "accolade fermante"],
+    ['-', "tiret"],
+    ['—', "tiret cadratin"],
+    ['–', "tiret demi-cadratin"],
+    ['«', "guillemet ouvrant (chevrons)"],
+    ['»', "guillemet fermant (chevrons)"],
+    ['“', "guillemet ouvrant double"],
+    ['”', "guillemet fermant double"],
+    ['‘', "guillemet ouvrant"],
+    ['’', "guillemet fermant"],
+    ['/', "signe de la division"],
+    ['+', "signe de l’addition"],
+    ['*', "signe de la multiplication"],
+    ['=', "signe de l’égalité"],
+    ['<', "inférieur à"],
+    ['>', "supérieur à"],
+]);
+
 
 class Lexicographe {
 
     constructor (oDict) {
         this.oDict = oDict;
@@ -165,71 +199,58 @@
         this._zElidedPrefix = new RegExp ("^([dljmtsncç]|quoiqu|lorsqu|jusqu|puisqu|qu)['’](.+)", "i");
         this._zCompoundWord = new RegExp ("([a-zA-Zà-ö0-9À-Öø-ÿØ-ßĀ-ʯ]+)-((?:les?|la)-(?:moi|toi|lui|[nv]ous|leur)|t-(?:il|elle|on)|y|en|[mts][’'](?:y|en)|les?|l[aà]|[mt]oi|leur|lui|je|tu|ils?|elles?|on|[nv]ous)$", "i");
         this._zTag = new RegExp ("[:;/][a-zA-Zà-ö0-9À-Öø-ÿØ-ßĀ-ʯ*][^:;/]*", "g");
     };
 
-    analyzeText (sText) {
-        sText = sText.replace(/[.,.?!:;…\/()\[\]“”«»"„{}–—#+*<>%=\n]/g, " ").replace(/\s+/g, " ");
-        let iStart = 0;
-        let iEnd = 0;
-        let sHtml = '<div class="paragraph">\n';
-        while ((iEnd = sText.indexOf(" ", iStart)) !== -1) {
-            sHtml += this.analyzeWord(sText.slice(iStart, iEnd));
-            iStart = iEnd + 1;
-        }
-        sHtml += this.analyzeWord(sText.slice(iStart));
-        return sHtml + '</div>\n';
-    }
-
-    analyzeWord (sWord) {
+    getInfoForToken (oToken) {
+        // Token: .sType, .sValue, .nStart, .nEnd
+        // return a list [type, token_string, values]
+        let m = null;
         try {
-            if (!sWord) {
-                return "";
-            }
-            if (sWord._count("-") > 4) {
-                return '<p><b class="mbok">' + sWord + "</b> <s>:</s> élément complexe indéterminé</p>\n";
-            }
-            if (sWord._isDigit()) {
-                return '<p><b class="nb">' + sWord + "</b> <s>:</s> nombre</p>\n";
-            }
-
-            let sHtml = "";
-            // préfixes élidés
-            let m = this._zElidedPrefix.exec(sWord);
-            if (m !== null) {
-                sWord = m[2];
-                sHtml += "<p><b>" + m[1] + "’</b> <s>:</s> " + _dPFX.get(m[1].toLowerCase()) + " </p>\n";
-            }
-            // mots composés
-            let m2 = this._zCompoundWord.exec(sWord);
-            if (m2 !== null) {
-                sWord = m2[1];
-            }
-            // Morphologies
-            let lMorph = this.oDict.getMorph(sWord);
-            if (lMorph.length === 1) {
-                sHtml += "<p><b>" + sWord + "</b> <s>:</s> " + this.formatTags(lMorph[0]) + "</p>\n";
-            } else if (lMorph.length > 1) {
-                sHtml += "<p><b>" + sWord + "</b><ul><li>" + [for (s of lMorph) if (s.includes(":")) this.formatTags(s)].join(" </li><li> ") + "</li></ul></p>\n";
-            } else {
-                sHtml += '<p><b class="unknown">' + sWord + "</b> <s>:</s>  absent du dictionnaire<p>\n";
-            }
-            // suffixe d’un mot composé
-            if (m2) {
-                sHtml += "<p>-<b>" + m2[2] + "</b> <s>:</s> " + this._formatSuffix(m2[2].toLowerCase()) + "</p>\n";
-            }
-            // Verbes
-            //let aVerb = new Set([ for (s of lMorph) if (s.includes(":V")) s.slice(1, s.indexOf(" ")) ]);
-            return sHtml;
+            switch (oToken.sType) {
+                case 'SEPARATOR':
+                    return { sType: oToken.sType, sValue: oToken.sValue, aLabel: [_dSeparator._get(oToken.sValue, "caractère indéterminé")] };
+                    break;
+                case 'NUM':
+                    return { sType: oToken.sType, sValue: oToken.sValue, aLabel: ["nombre"] };
+                    break;
+                case 'LINK':
+                    return { sType: oToken.sType, sValue: oToken.sValue.slice(0,40)+"…", aLabel: ["hyperlien"] };
+                    break;
+                case 'ELPFX':
+                    let sTemp = oToken.sValue.replace("’", "").replace("'", "").replace("`", "").toLowerCase();
+                    return { sType: oToken.sType, sValue: oToken.sValue, aLabel: [_dPFX._get(sTemp, "préfixe élidé inconnu")] };
+                    break;
+                case 'WORD': 
+                    if (oToken.sValue._count("-") > 4) {
+                        return { sType: "COMPLEX", sValue: oToken.sValue, aLabel: ["élément complexe indéterminé"] };
+                    }
+                    else if (this.oDict.isValidToken(oToken.sValue)) {
+                        let lMorph = this.oDict.getMorph(oToken.sValue);
+                        let aElem = [ for (s of lMorph) if (s.includes(":")) this._formatTags(s) ];
+                        return { sType: oToken.sType, sValue: oToken.sValue, aLabel: aElem};
+                    }
+                    else if (m = this._zCompoundWord.exec(oToken.sValue)) {
+                        // mots composés
+                        let lMorph = this.oDict.getMorph(m[1]);
+                        let aElem = [ for (s of lMorph) if (s.includes(":")) this._formatTags(s) ];
+                        aElem.push("-" + m[2] + ": " + this._formatSuffix(m[2].toLowerCase()));
+                        return { sType: oToken.sType, sValue: oToken.sValue, aLabel: aElem };
+                    }
+                    else {
+                        return { sType: "UNKNOWN", sValue: oToken.sValue, aLabel: ["inconnu du dictionnaire"] };
+                    }
+                    break;
+            }
         }
         catch (e) {
             helpers.logerror(e);
-            return "#erreur";
         }
+        return null;
     };
 
-    formatTags (sTags) {
+    _formatTags (sTags) {
         let sRes = "";
         sTags = sTags.replace(/V([0-3][ea]?)[itpqnmr_eaxz]+/, "V$1");
         let m;
         while ((m = this._zTag.exec(sTags)) !== null) {
             sRes += _dTAGS.get(m[0]);

Index: gc_lang/fr/xpi/data/gc_panel.css
==================================================================
--- gc_lang/fr/xpi/data/gc_panel.css
+++ gc_lang/fr/xpi/data/gc_panel.css
@@ -8,12 +8,10 @@
     border-bottom: 1px solid hsl(0, 0%, 90%);
     color: hsl(0, 0%, 0%);
     z-index: 99;
 }
 
-
-
 body {
 	background-color: hsl(0, 0%, 98%);
 	font-family: Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
 	overflow-x: hidden;
     color: hsl(0, 0%, 0%);
@@ -69,14 +67,19 @@
     font-size: 18px;
     color: hsla(240, 0%, 96%, 1);
     border-radius: 3px;
     text-align: center;
 }
+
 #errorlist p.green {
     background-color: hsla(120, 10%, 50%, 1);
     color: hsla(120, 0%, 96%, 1);
 }
+
+.paragraph_block {
+    margin: 0 0 30px 0;
+}
 
 .paragraph {
     background-color: hsla(0, 0%, 90%, 1);
     padding: 10px;
     border-radius: 2px;
@@ -103,38 +106,10 @@
     background-color: hsla(210, 60%, 40%, 1);
     color: hsla(0, 0%, 100%, 1);
     text-shadow: 0 0 3px hsl(210, 30%, 60%);
 }
 
-.paragraph a.sugg {
-    padding: 1px 6px;
-    background-color: hsla(150, 50%, 40%, 1);
-    color: hsla(150, 0%, 96%, 1);
-    border-radius: 2px;
-    cursor: pointer;
-    text-decoration: none;
-}
-.paragraph a.sugg:hover {
-    background-color: hsla(150, 70%, 30%, 1);
-    color: hsla(0, 0%, 100%, 1);
-    text-shadow: 0 0 3px hsl(150, 30%, 60%);
-}
-
-.paragraph a.ignore {
-    padding: 0 2px;
-    background-color: hsla(30, 20%, 60%, 1);
-    color: hsla(30, 0%, 96%, 1);
-    border-radius: 2px;
-    cursor: pointer;
-    text-decoration: none;
-}
-.paragraph a.ignore:hover {
-    background-color: hsla(30, 20%, 50%, 1);
-    color: hsla(0, 0%, 100%, 1);
-    text-shadow: 0 0 3px hsl(30, 30%, 60%);
-}
-
 .paragraph u.corrected,
 .paragraph u.ignored {
     background-color: hsla(120, 50%, 70%, 1);
     color: hsla(0, 0%, 4%, 1);
     border-radius: 2px;
@@ -152,58 +127,139 @@
 }
 .paragraph u.error:hover {
     cursor: pointer;
 }
 
-.paragraph u.error .tooltip, .paragraph u.error .tooltip_on {
-    position: absolute;
-    background-color: hsla(210, 10%, 90%, 1);
-    font-family: Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
-    top: 90%;
-    left: 0;
-    width: 250px;
-    font-size: 12px;
-    line-height: 18px;
-    color: hsla(0, 10%, 20%, 1);
-    cursor: default;
-    /*visibility: hidden;*/
-    display: none;
-    padding: 10px;
-    box-shadow: 0 0 6px hsla(0, 0%, 0%, 0.3);
-    z-index: 10;
-    border: 2px solid hsl(0, 0%, 0%);
-    border-radius: 3px;
-    text-decoration: none;
-}
-.paragraph u.error .tooltip_on {
-    display: block;
-}
-
-.tooltip_on s {
-    color: hsla(0, 0%, 66%, 1);
-    font-weight: bold;
-    font-size: 8px;
-    line-height: 16px;
-    text-transform: uppercase;
-    text-decoration: none;
-}
-
-.debug {
-    float: right;
+
+/* 
+    TOOLTIPS
+*/
+.tooltip {
+    position: absolute;
+    display: none;
+    width: 300px;
+    border-radius: 5px;
+    box-shadow: 0 0 6px hsla(0, 0%, 0%, 0.3);
+    font-family: Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
+    font-size: 12px;
+    line-height: 18px;
+    cursor: default;
+    z-index: 10;
+    text-decoration: none;
+}
+#gc_tooltip {
+    border: 3px solid hsl(210, 50%, 30%);
+    color: hsla(210, 10%, 20%, 1);
+}
+#sc_tooltip {
+    border: 3px solid hsl(0, 50%, 30%);
+    color: hsla(0, 10%, 20%, 1);
+}
+#gc_tooltip #gc_rule_id {
+    display: none;
     background-color: hsla(0, 5%, 35%, 1);
     padding: 2px 5px;
     margin-left: 5px;
     border-radius: 2px;
     color: hsla(0, 0%, 96%, 1);
     font-size: 11px;
     font-style: normal;
 }
-
-.data {
-    font-style: normal;
+#gc_message_block {
+    padding: 5px 10px 10px 10px;
+    background-color: hsl(210, 50%, 30%);
+    color: hsl(210, 50%, 96%);
+}
+#sc_message_block {
+    padding: 5px 10px 10px 10px;
+    background-color: hsl(0, 50%, 30%);
+    color: hsl(0, 50%, 96%);
+}
+#gc_message, #sc_message {
+    font-size: 15px;
+    margin-bottom: 5px;
+}
+a#gc_ignore, a#sc_ignore {
+    padding: 0 2px;
+    background-color: hsla(30, 30%, 40%, 1);
+    color: hsla(30, 0%, 96%, 1);
+    border-radius: 2px;
+    cursor: pointer;
+    text-decoration: none;
+}
+a#gc_ignore:hover, a#sc_ignore:hover {
+    background-color: hsla(30, 30%, 50%, 1);
+    color: hsla(0, 0%, 100%, 1);
+    text-shadow: 0 0 3px hsl(30, 30%, 60%);
+}
+a#gc_url {
+    padding: 0 2px;
+    background-color: hsla(210, 50%, 50%, 1);
+    color: hsla(210, 0%, 96%, 1);
+    border-radius: 2px;
+    cursor: pointer;
+    text-decoration: none;
+}
+a#gc_url:hover {
+    background-color: hsla(210, 50%, 60%, 1);
+    color: hsla(0, 0%, 100%, 1);
+    text-shadow: 0 0 3px hsl(210, 30%, 60%);
+}
+#gc_sugg_title {
+    padding: 0 10px;
+    background-color: hsl(210, 10%, 90%);
+    color: hsl(210, 50%, 30%);
+    font-size: 10px;
+    font-weight: bold;
+}
+#sc_sugg_title {
+    padding: 0 10px;
+    background-color: hsl(0, 10%, 90%);
+    color: hsl(0, 50%, 30%);
+    font-size: 9px;
+    font-weight: bold;
+}
+#gc_sugg_block {
+    padding: 10px;
+    background-color: hsl(210, 10%, 96%);
+    border-radius: 0 0 2px 2px;
+}
+#sc_sugg_block {
+    padding: 10px;
+    background-color: hsl(0, 10%, 96%);
+    border-radius: 0 0 2px 2px;
+}
+#gc_sugg_block a.sugg {
+    padding: 1px 6px;
+    background-color: hsla(180, 50%, 40%, 1);
+    color: hsla(180, 0%, 96%, 1);
+    border-radius: 2px;
+    cursor: pointer;
+    text-decoration: none;
+}
+#gc_sugg_block a.sugg:hover {
+    background-color: hsla(180, 70%, 50%, 1);
+    color: hsla(0, 0%, 100%, 1);
+    text-shadow: 0 0 3px hsl(180, 30%, 60%);
+}
+#sc_sugg_block a.sugg {
+    padding: 1px 6px;
+    background-color: hsla(30, 80%, 40%, 1);
+    color: hsla(30, 0%, 96%, 1);
+    border-radius: 2px;
+    cursor: pointer;
+    text-decoration: none;
+}
+#sc_sugg_block a.sugg:hover {
+    background-color: hsla(30, 100%, 50%, 1);
+    color: hsla(0, 0%, 100%, 1);
+    text-shadow: 0 0 3px hsl(30, 30%, 60%);
 }
 
+/*
+    Action buttons
+*/
 
 .actions {
     margin-top: -10px;
     margin-bottom: 10px;
 }
@@ -266,16 +322,16 @@
     background-color: hsl(240, 10%, 40%);
     color: hsl(240, 0%, 100%);
 }
 
 /* elems */
-.spell {
+.WORD {
     background-color: hsl(0, 50%, 50%);
     color: hsl(0, 0%, 96%);
     /*text-decoration: underline wavy hsl(0, 50%, 50%);*/
 }
-.spell:hover {
+.WORD:hover {
     background-color: hsl(0, 60%, 40%);
     color: hsl(0, 0%, 100%);
 }
 
 /* elems */

Index: gc_lang/fr/xpi/data/gc_panel.html
==================================================================
--- gc_lang/fr/xpi/data/gc_panel.html
+++ gc_lang/fr/xpi/data/gc_panel.html
@@ -49,10 +49,31 @@
 
         <div id="errorlist">
             <!-- result comes here -->
         </div>
 
+        <div id="gc_tooltip" class="tooltip">
+            <!-- grammar error -->
+            <div id="gc_message_block">
+                <p id="gc_rule_id"></p>
+                <p id="gc_message">Erreur grammaticale.</p>
+                <a id="gc_ignore" href="#">Ignorer</a> &nbsp; 
+                <a id="gc_url" href="">Voulez-vous en savoir plus ?…</a>
+            </div>
+            <div id="gc_sugg_title">SUGGESTIONS :</div>
+            <div id="gc_sugg_block"></div>
+        </div>
+
+        <div id="sc_tooltip" class="tooltip">
+            <!-- spelling error -->
+            <div id="sc_message_block">
+                <p id="sc_message">Mot inconnu du dictionnaire.</p>
+                <a id="sc_ignore" href="#">Ignorer</a>
+            </div>
+            <div id="sc_sugg_title">SUGGESTIONS :</div>
+            <div id="sc_sugg_block"></div>
+        </div>
     </body>
 </html>
 
 
         

Index: gc_lang/fr/xpi/data/gc_panel.js
==================================================================
--- gc_lang/fr/xpi/data/gc_panel.js
+++ gc_lang/fr/xpi/data/gc_panel.js
@@ -2,272 +2,424 @@
 
 let nPanelWidth = 0;  // must be set at launch
 let bExpanded = true;
 
 /*
-	Events
+    Events
 */
 
-if (Date.now() < Date.UTC(2017, 6, 12)) {
-	try {
-		document.getElementById('special_message').style.display = "block";
-		document.getElementById('errorlist').style.padding = "20px 20px 30px 20px";
-	} catch (e) {
-		console.log(e.message + e.lineNumber);
-	}
-}
+showSpecialMessage();
 
 
 document.getElementById('close').addEventListener("click", function (event) {
-	bExpanded = true; // size is reset in ui.js
-	self.port.emit('closePanel');
+    bExpanded = true; // size is reset in ui.js
+    self.port.emit('closePanel');
 });
 
 document.getElementById('expand_reduce').addEventListener("click", function (event) {
-	if (bExpanded) {
-		self.port.emit("resize", "reduce", 10); // the number has no meaning here
-		bExpanded = false;
-	} else {
-		self.port.emit("resize", "expand", 10); // the number has no meaning here
-		bExpanded = true;
-	}
+    if (bExpanded) {
+        self.port.emit("resize", "reduce", 10); // the number has no meaning here
+        bExpanded = false;
+    } else {
+        self.port.emit("resize", "expand", 10); // the number has no meaning here
+        bExpanded = true;
+    }
 });
 
 document.getElementById('copy_to_clipboard').addEventListener("click", function (event) {
-	copyToClipboard();
+    copyToClipboard();
 });
 
 document.getElementById('closemsg').addEventListener("click", function (event) {
-	closeMessageBox();
+    closeMessageBox();
 });
 
 self.port.on("setPanelWidth", function (n) {
-	nPanelWidth = n;
+    nPanelWidth = n;
+});
+
+self.port.on("addMessage", function (sClass, sText) {
+    addMessage(sClass, sText);
 });
 
-self.port.on("addElem", function (sHtml) {
-	let xElem = document.createElement("div");
-	xElem.innerHTML = sHtml;
-	document.getElementById("errorlist").appendChild(xElem);
+self.port.on("addParagraph", function (sText, iParagraph, sJSON) {
+    addParagraph(sText, iParagraph, sJSON);
 });
 
-self.port.on("refreshParagraph", function (sIdParagr, sHtml) {
-	document.getElementById("paragr"+sIdParagr).innerHTML = sHtml;
-	let sClassName = (sHtml.includes('<u id="err')) ? "paragraph softred" : "paragraph softgreen";
-	document.getElementById("paragr"+sIdParagr).className = sClassName;
+self.port.on("refreshParagraph", function (sText, sIdParagr, sJSON) {
+    refreshParagraph(sText, sIdParagr, sJSON);
 });
 
 self.port.on("showMessage", function (sText) {
-	document.getElementById("message").textContent = sText;
-	document.getElementById("messagebox").style.display = "block";
-	window.setTimeout(closeMessageBox, 20000);
+    document.getElementById("message").textContent = sText;
+    document.getElementById("messagebox").style.display = "block";
+    window.setTimeout(closeMessageBox, 20000);
 });
 
 self.port.on("clearErrors", function (sHtml) {
-	document.getElementById("errorlist").textContent = "";
+    document.getElementById("errorlist").textContent = "";
+    hideAllTooltips();
 });
 
 self.port.on("start", function() {
-	startWaitIcon();
+    startWaitIcon();
 });
 
 self.port.on("end", function() {
-	stopWaitIcon();
-	document.getElementById("copy_to_clipboard").style.display = "block";
+    stopWaitIcon();
+    document.getElementById("copy_to_clipboard").style.display = "block";
 });
 
-self.port.on("suggestionsFor", function (sWord, sSuggestions, sTooltipId) {
-	// spell checking suggestions
-	//console.log(sWord + ": " + sSuggestions);
-	if (sSuggestions === "") {
-		document.getElementById(sTooltipId).innerHTML += "Aucune.";
-	} else if (sSuggestions.startsWith("#")) {
-		document.getElementById(sTooltipId).innerHTML += sSuggestions;
-	} else {
-		let lSugg = sSuggestions.split("|");
-		let iSugg = 0;
-		let sElemId = sTooltipId.slice(7);
-		for (let sSugg of lSugg) {
-			document.getElementById(sTooltipId).innerHTML += '<a id="sugg' + sElemId + "-" + iSugg + '" class="sugg" href="#" onclick="return false;">' + sSugg + '</a> ';
-			iSugg += 1;
-		}
-	}
+self.port.on("suggestionsFor", function (sWord, sSuggestions, sErrId) {
+    setSpellSuggestionsFor(sWord, sSuggestions, sErrId);
 });
 
 
 window.addEventListener(
-	"click",
-	function (xEvent) {
-		let xElem = xEvent.target;
-		if (xElem.id) {
-			if (xElem.id.startsWith("sugg")) {
-				applySuggestion(xElem.id);
-			} else if (xElem.id.startsWith("ignore")) {
-				ignoreError(xElem.id);
-			} else if (xElem.id.startsWith("check")) {
-				sendBackAndCheck(xElem.id);
-			} else if (xElem.id.startsWith("edit")) {
-				switchEdition(xElem.id);
-			} else if (xElem.id.startsWith("end")) {
-				document.getElementById(xElem.id).parentNode.parentNode.style.display = "none";
-			} else if (xElem.tagName === "U" && xElem.id.startsWith("err")) {
-				showTooltip(xElem.id);
-			} else if (xElem.id.startsWith("resize")) {
-				self.port.emit("resize", xElem.id, 10);
-			} else {
-				hideAllTooltips();
-			}
-		} else if (xElem.tagName === "A") {
-			self.port.emit("openURL", xElem.getAttribute("href"));
-		} else {
-			hideAllTooltips();
-		}
-	},
-	false
+    "click",
+    function (xEvent) {
+        try {
+            let xElem = xEvent.target;
+            if (xElem.id) {
+                if (xElem.id.startsWith("sugg")) {
+                    applySuggestion(xElem.id);
+                } else if (xElem.id.endsWith("_ignore")) {
+                    ignoreError(xElem.id);
+                } else if (xElem.id.startsWith("check")) {
+                    sendBackAndCheck(xElem.id);
+                } else if (xElem.id.startsWith("edit")) {
+                    switchEdition(xElem.id);
+                } else if (xElem.id.startsWith("end")) {
+                    document.getElementById(xElem.id).parentNode.parentNode.style.display = "none";
+                } else if (xElem.tagName === "U" && xElem.id.startsWith("err")
+                           && xElem.className !== "corrected" && xElem.className !== "ignored") {
+                    showTooltip(xElem.id);
+                } else if (xElem.id.startsWith("resize")) {
+                    self.port.emit("resize", xElem.id, 10);
+                } else {
+                    hideAllTooltips();
+                }
+            } else if (xElem.tagName === "A") {
+                self.port.emit("openURL", xElem.getAttribute("href"));
+            } else {
+                hideAllTooltips();
+            }
+        }
+        catch (e) {
+            showError(e);
+        }
+    },
+    false
 );
 
 
 /*
-	Actions
+    Actions
 */
+
+function showError (e) {
+    console.error("\n" + e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+}
 
 function closeMessageBox () {
-	document.getElementById("messagebox").style.display = "none";
-	document.getElementById("message").textContent = "";
-}
-
-function applySuggestion (sElemId) { // sugg
-	try {
-		let sIdParagr = sElemId.slice(4, sElemId.indexOf("_"));
-		startWaitIcon("paragr"+sIdParagr);
-		let sIdErr = "err" + sElemId.slice(4, sElemId.indexOf("-"));
-		document.getElementById(sIdErr).textContent = document.getElementById(sElemId).textContent;
-		document.getElementById(sIdErr).className = "corrected";
-		document.getElementById(sIdErr).removeAttribute("style");
-		self.port.emit("correction", sIdParagr, getPurgedTextOfElem("paragr"+sIdParagr));
-		stopWaitIcon("paragr"+sIdParagr);
-	} catch (e) {
-		console.log(e.message + e.lineNumber);
-	}
-}
-
-function ignoreError (sElemId) {  // ignore
-	let sIdErr = "err" + sElemId.slice(6);
-	let xTooltipElem = document.getElementById("tooltip"+sElemId.slice(6));
-	document.getElementById(sIdErr).removeChild(xTooltipElem);
-	document.getElementById(sIdErr).className = "ignored";
-	document.getElementById(sIdErr).removeAttribute("style");
-}
-
-function showTooltip (sElemId) {  // err
-	hideAllTooltips();
-	let sTooltipId = "tooltip" + sElemId.slice(3);
-	let xTooltipElem = document.getElementById(sTooltipId);
-	let nLimit = nPanelWidth - 300; // paragraph width - tooltip width
-	if (document.getElementById(sElemId).offsetLeft > nLimit) {
-		xTooltipElem.style.left = "-" + (document.getElementById(sElemId).offsetLeft - nLimit) + "px";
-	}
-	xTooltipElem.setAttribute("contenteditable", false);
-	xTooltipElem.className = 'tooltip_on';
-	if (document.getElementById(sElemId).className === "error spell"  &&  xTooltipElem.textContent.endsWith(":")) {
-		// spelling mistake
-		self.port.emit("getSuggestionsForTo", document.getElementById(sElemId).innerHTML.replace(/<span .*$/, "").trim(), sTooltipId);
-	}
-}
-
-function switchEdition (sElemId) {  // edit
-	let sId = "paragr" + sElemId.slice(4);
-	if (document.getElementById(sId).hasAttribute("contenteditable") === false
-		|| document.getElementById(sId).getAttribute("contenteditable") === "false") {
-		document.getElementById(sId).setAttribute("contenteditable", true);
-		document.getElementById(sElemId).className = "button orange";
-		document.getElementById(sId).focus();
-	} else {
-		document.getElementById(sId).setAttribute("contenteditable", false);
-		document.getElementById(sElemId).className = "button";
-	}
-}
-
-function sendBackAndCheck (sElemId) {  // check
-	startWaitIcon();
-	let sIdParagr = sElemId.slice(5);
-	self.port.emit("modifyAndCheck", sIdParagr, getPurgedTextOfElem("paragr"+sIdParagr));
-	stopWaitIcon();
-}
-
-function getPurgedTextOfElem (sId) {
-	// Note : getPurgedTextOfElem2 should work better if not buggy.
-	// if user writes in error, Fx adds tags <u></u>, we also remove style attribute
-	let xParagraphElem = document.getElementById(sId);
-	for (xNode of xParagraphElem.getElementsByTagName("u")) {
-		if (xNode.id.startsWith('err')) {
-			xNode.innerHTML = xNode.innerHTML.replace(/<\/?u>/g, "");
-			xNode.removeAttribute("style");
-		}
-	}
-	// we remove style attribute on tooltip
-	for (xNode of xParagraphElem.getElementsByTagName("span")) {
-		if (xNode.id.startsWith('tooltip')) {
-			xNode.removeAttribute("style");
-		}
-	}
-	// now, we remove tooltips, then errors, and we change some html entities
-	let sText = xParagraphElem.innerHTML;
-	sText = sText.replace(/<br\/? *> *$/ig, "");
-	sText = sText.replace(/<span id="tooltip.+?<\/span>/g, "");
-	sText = sText.replace(/<u id="err\w+" class="[\w ]+" href="#" onclick="return false;">(.+?)<\/u><!-- err_end -->/g, "$1");
-	sText = sText.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
-	return sText;
-}
-
-function getPurgedTextOfElem2 (sId) {
-	// It is better to remove tooltips via DOM and retrieve textContent,
-	// but for some reason getElementsByClassName “hazardously” forgets elements.
-	// Unused. Needs investigation.
-	let xParagraphElem = document.getElementById(sId).cloneNode(true);
-	for (let xNode of xParagraphElem.getElementsByClassName("tooltip")) {
-		xNode.parentNode.removeChild(xNode);
-	}
-	return xParagraphElem.textContent;
+    document.getElementById("messagebox").style.display = "none";
+    document.getElementById("message").textContent = "";
+}
+
+function addMessage (sClass, sText) {
+    let xNode = document.createElement("p");
+    xNode.className = sClass;
+    xNode.textContent = sText;
+    document.getElementById("errorlist").appendChild(xNode);
+}
+
+function addParagraph (sText, iParagraph, sJSON) {
+    try {
+        let xNodeDiv = document.createElement("div");
+        xNodeDiv.className = "paragraph_block";
+        // paragraph
+        let xParagraph = document.createElement("p");
+        xParagraph.id = "paragr" + iParagraph.toString();
+        xParagraph.lang = "fr";
+        xParagraph.setAttribute("spellcheck", false);
+        let oErrors = JSON.parse(sJSON);
+        xParagraph.className = (oErrors.aGrammErr.length || oErrors.aSpellErr.length) ? "paragraph softred" : "paragraph";
+        _tagParagraph(sText, xParagraph, iParagraph, oErrors.aGrammErr, oErrors.aSpellErr);
+        xNodeDiv.appendChild(xParagraph);
+        // actions
+        let xDivActions = document.createElement("div");
+        xDivActions.className = "actions";
+        let xDivClose = document.createElement("div");
+        xDivClose.id = "end" + iParagraph.toString();
+        xDivClose.className = "button red";
+        xDivClose.textContent = "×";
+        let xDivEdit = document.createElement("div");
+        xDivEdit.id = "edit" + iParagraph.toString();
+        xDivEdit.className = "button";
+        xDivEdit.textContent = "Éditer";
+        let xDivCheck = document.createElement("div");
+        xDivCheck.id = "check" + iParagraph.toString();
+        xDivCheck.className = "button green";
+        xDivCheck.textContent = "Réanalyser";
+        xDivActions.appendChild(xDivClose);
+        xDivActions.appendChild(xDivEdit);
+        xDivActions.appendChild(xDivCheck);
+        xNodeDiv.appendChild(xDivActions);
+        document.getElementById("errorlist").appendChild(xNodeDiv);
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function refreshParagraph (sText, sIdParagr, sJSON) {
+    try {
+        let xParagraph = document.getElementById("paragr"+sIdParagr);
+        let oErrors = JSON.parse(sJSON);
+        xParagraph.className = (oErrors.aGrammErr.length || oErrors.aSpellErr.length) ? "paragraph softred" : "paragraph softgreen";
+        xParagraph.textContent = "";
+        _tagParagraph(sText, xParagraph, sIdParagr, oErrors.aGrammErr, oErrors.aSpellErr);
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function _tagParagraph (sParagraph, xParagraph, iParagraph, aSpellErr, aGrammErr) {
+    try {
+        if (aGrammErr.length === 0  &&  aSpellErr.length === 0) {
+            xParagraph.textContent = sParagraph;
+            return
+        }
+        aGrammErr.push(...aSpellErr);
+        aGrammErr.sort(function (a, b) {
+            if (a["nStart"] < b["nStart"])
+                return -1;
+            if (a["nStart"] > b["nStart"])
+                return 1;
+            return 0;
+        });
+
+        let nErr = 0; // we count errors to give them an identifier
+        let nEndLastErr = 0;
+        for (let oErr of aGrammErr) {
+            let nStart = oErr["nStart"];
+            let nEnd = oErr["nEnd"];
+            if (nStart >= nEndLastErr) {
+                oErr["sId"] = iParagraph.toString() + "_" + nErr.toString(); // error identifier
+                if (nEndLastErr < nStart) {
+                    xParagraph.appendChild(document.createTextNode(sParagraph.slice(nEndLastErr, nStart)));
+                }
+                xParagraph.appendChild(_createError(sParagraph.slice(nStart, nEnd), oErr));
+                xParagraph.insertAdjacentHTML("beforeend", "<!-- err_end -->");
+                nEndLastErr = nEnd;
+            }
+            nErr += 1;
+        }
+        if (nEndLastErr <= sParagraph.length) {
+            xParagraph.appendChild(document.createTextNode(sParagraph.slice(nEndLastErr)));
+        }
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function _createError (sUnderlined, oErr) {
+    let xNodeErr = document.createElement("u");
+    xNodeErr.id = "err" + oErr['sId'];
+    xNodeErr.className = "error " + oErr['sType'];
+    xNodeErr.textContent = sUnderlined;
+    xNodeErr.dataset.error_id = oErr['sId'];
+    xNodeErr.dataset.error_type = (oErr['sType'] === "WORD") ? "spelling" : "grammar";
+    xNodeErr.setAttribute("href", "#");
+    xNodeErr.setAttribute("onclick", "return false;");
+    if (xNodeErr.dataset.error_type === "grammar") {
+        xNodeErr.dataset.gc_message = oErr['sMessage'];
+        xNodeErr.dataset.gc_url = oErr['URL'];
+        if (xNodeErr.dataset.gc_message.includes(" #")) {
+            xNodeErr.dataset.line_id = oErr['sLineId'];
+            xNodeErr.dataset.rule_id = oErr['sRuleId'];
+        }
+        xNodeErr.dataset.suggestions = oErr["aSuggestions"].join("|");
+    }
+    return xNodeErr;
+}
+
+function applySuggestion (sSuggId) { // sugg
+    try {
+        let sErrorId = document.getElementById(sSuggId).dataset.error_id;
+        let sIdParagr = sErrorId.slice(0, sErrorId.indexOf("_"));
+        startWaitIcon("paragr"+sIdParagr);
+        let xNodeErr = document.getElementById("err" + sErrorId);
+        xNodeErr.textContent = document.getElementById(sSuggId).textContent;
+        xNodeErr.className = "corrected";
+        xNodeErr.removeAttribute("style");
+        self.port.emit("correction", sIdParagr, document.getElementById("paragr"+sIdParagr).textContent);
+        hideAllTooltips();
+        stopWaitIcon("paragr"+sIdParagr);
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function ignoreError (sIgnoreButtonId) {  // ignore
+    try {
+        console.log("ignore button: " + sIgnoreButtonId + " // error id: " + document.getElementById(sIgnoreButtonId).dataset.error_id);
+        let xNodeErr = document.getElementById("err"+document.getElementById(sIgnoreButtonId).dataset.error_id);
+        xNodeErr.className = "ignored";
+        xNodeErr.removeAttribute("style");
+        hideAllTooltips();
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function showTooltip (sNodeErrorId) {  // err
+    try {
+        hideAllTooltips();
+        let xNodeError = document.getElementById(sNodeErrorId);
+        let sTooltipId = (xNodeError.dataset.error_type === "grammar") ? "gc_tooltip" : "sc_tooltip";
+        let xNodeTooltip = document.getElementById(sTooltipId);
+        let nLimit = nPanelWidth - 330; // paragraph width - tooltip width
+        xNodeTooltip.style.top = (xNodeError.offsetTop + 16) + "px";
+        xNodeTooltip.style.left = (xNodeError.offsetLeft > nLimit) ? nLimit + "px" : xNodeError.offsetLeft + "px";
+        if (xNodeError.dataset.error_type === "grammar") {
+            // grammar error
+            document.getElementById("gc_message").textContent = xNodeError.dataset.gc_message;
+            if (xNodeError.dataset.gc_url != "") {
+                document.getElementById("gc_url").style.display = "inline";
+                document.getElementById("gc_url").setAttribute("href", xNodeError.dataset.gc_url);
+            } else {
+                document.getElementById("gc_url").style.display = "none";
+            }
+            document.getElementById("gc_ignore").dataset.error_id = xNodeError.dataset.error_id;
+            let iSugg = 0;
+            let xGCSugg = document.getElementById("gc_sugg_block");
+            xGCSugg.textContent = "";
+            for (let sSugg of xNodeError.dataset.suggestions.split("|")) {
+                xGCSugg.appendChild(_createSuggestion(xNodeError.dataset.error_id, iSugg, sSugg));
+                xGCSugg.appendChild(document.createTextNode(" "));
+                iSugg += 1;
+            }
+        }
+        xNodeTooltip.style.display = "block";
+        if (xNodeError.dataset.error_type === "spelling") {
+            // spelling mistake
+            document.getElementById("sc_ignore").dataset.error_id = xNodeError.dataset.error_id;
+            //console.log("getSuggFor: " + xNodeError.textContent.trim() + " // error_id: " + xNodeError.dataset.error_id);
+            self.port.emit("getSuggestionsForTo", xNodeError.textContent.trim(), xNodeError.dataset.error_id);
+        }
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function _createSuggestion (sErrId, iSugg, sSugg) {
+    let xNodeSugg = document.createElement("a");
+    xNodeSugg.id = "sugg" + sErrId + "-" + iSugg.toString();
+    xNodeSugg.className = "sugg";
+    xNodeSugg.setAttribute("href", "#");
+    xNodeSugg.setAttribute("onclick", "return false;");
+    xNodeSugg.dataset.error_id = sErrId;
+    xNodeSugg.textContent = sSugg;
+    return xNodeSugg;
+}
+
+function switchEdition (sEditButtonId) {  // edit
+    let xParagraph = document.getElementById("paragr" + sEditButtonId.slice(4));
+    if (xParagraph.hasAttribute("contenteditable") === false
+        || xParagraph.getAttribute("contenteditable") === "false") {
+        xParagraph.setAttribute("contenteditable", true);
+        document.getElementById(sEditButtonId).className = "button orange";
+        xParagraph.focus();
+    } else {
+        xParagraph.setAttribute("contenteditable", false);
+        document.getElementById(sEditButtonId).className = "button";
+    }
+}
+
+function sendBackAndCheck (sCheckButtonId) {  // check
+    startWaitIcon();
+    let sIdParagr = sCheckButtonId.slice(5);
+    self.port.emit("modifyAndCheck", sIdParagr, document.getElementById("paragr"+sIdParagr).textContent);
+    stopWaitIcon();
 }
 
 function hideAllTooltips () {
-	for (let xElem of document.getElementsByClassName("tooltip_on")) {
-		xElem.className = "tooltip";
-		xElem.removeAttribute("style");
-	}
+    document.getElementById("gc_tooltip").style.display = "none";
+    document.getElementById("sc_tooltip").style.display = "none";
+}
+
+function setSpellSuggestionsFor (sWord, sSuggestions, sErrId) {
+    // spell checking suggestions
+    try {
+        // console.log("setSuggestionsFor: " + sWord + " > " + sSuggestions + " // " + sErrId);
+        let xSuggBlock = document.getElementById("sc_sugg_block");
+        xSuggBlock.textContent = "";
+        if (sSuggestions === "") {
+            xSuggBlock.appendChild(document.createTextNode("Aucune."));
+        } else if (sSuggestions.startsWith("#")) {
+            xSuggBlock.appendChild(document.createTextNode(sSuggestions));
+        } else {
+            let lSugg = sSuggestions.split("|");
+            let iSugg = 0;
+            for (let sSugg of lSugg) {
+                xSuggBlock.appendChild(_createSuggestion(sErrId, iSugg, sSugg));
+                xSuggBlock.appendChild(document.createTextNode(" "));
+                iSugg += 1;
+            }
+        }
+    }
+    catch (e) {
+        showError(e);
+    }
 }
 
 function copyToClipboard () {
-	startWaitIcon();
-	try {
-		document.getElementById("clipboard_msg").textContent = "copie en cours…";
-		let sText = "";
-		for (let xNode of document.getElementById("errorlist").getElementsByClassName("paragraph")) {
-			sText += getPurgedTextOfElem(xNode.id);
-			sText += "\n";
-		}
-		self.port.emit('copyToClipboard', sText);
-		document.getElementById("clipboard_msg").textContent = "-> presse-papiers";
-		window.setTimeout(function() { document.getElementById("clipboard_msg").textContent = "∑"; } , 3000);
-	}
-	catch (e) {
-		console.log(e.lineNumber + ": " +e.message);
-	}
-	stopWaitIcon();
+    startWaitIcon();
+    try {
+        let xClipboardButton = document.getElementById("clipboard_msg");
+        xClipboardButton.textContent = "copie en cours…";
+        let sText = "";
+        for (let xNode of document.getElementById("errorlist").getElementsByClassName("paragraph")) {
+            sText += xNode.textContent + "\n";
+        }
+        self.port.emit('copyToClipboard', sText);
+        xClipboardButton.textContent = "-> presse-papiers";
+        window.setTimeout(function() { xClipboardButton.textContent = "∑"; } , 3000);
+    }
+    catch (e) {
+        console.log(e.lineNumber + ": " +e.message);
+    }
+    stopWaitIcon();
 }
 
 function startWaitIcon (sIdParagr=null) {
-	if (sIdParagr) {
-		document.getElementById(sIdParagr).disabled = true;
-		document.getElementById(sIdParagr).style.opacity = .3;
-	}
-	document.getElementById("waiticon").hidden = false;
+    if (sIdParagr) {
+        document.getElementById(sIdParagr).disabled = true;
+        document.getElementById(sIdParagr).style.opacity = .3;
+    }
+    document.getElementById("waiticon").hidden = false;
 }
 
 function stopWaitIcon (sIdParagr=null) {
-	if (sIdParagr) {
-		document.getElementById(sIdParagr).disabled = false;
-		document.getElementById(sIdParagr).style.opacity = 1;
-	}
-	document.getElementById("waiticon").hidden = true;
+    if (sIdParagr) {
+        document.getElementById(sIdParagr).disabled = false;
+        document.getElementById(sIdParagr).style.opacity = 1;
+    }
+    document.getElementById("waiticon").hidden = true;
+}
+
+function showSpecialMessage () {
+    if (Date.now() < Date.UTC(2017, 6, 12)) {
+        try {
+            document.getElementById('special_message').style.display = "block";
+            document.getElementById('errorlist').style.padding = "20px 20px 30px 20px";
+        } catch (e) {
+            showError(e);
+        }
+    }
 }

Index: gc_lang/fr/xpi/data/lxg_panel.css
==================================================================
--- gc_lang/fr/xpi/data/lxg_panel.css
+++ gc_lang/fr/xpi/data/lxg_panel.css
@@ -40,28 +40,43 @@
     color: hsla(0, 0%, 96%, 1);
     border-radius: 5px;
     text-align: center;
     font-size: 20px;
 }
+#wordlist .token {
+    margin: 8px;
+}
 #wordlist ul {
     margin: 0 0 5px 40px;
 }
 #wordlist b {
-    background-color: hsla(150, 50%, 50%, 1);
+    background-color: hsla(150, 10%, 50%, 1);
     color: hsla(0, 0%, 96%, 1);
     padding: 2px 5px;
     border-radius: 2px;
     text-decoration: none;
 }
-#wordlist b.unknown {
+#wordlist b.WORD {
+    background-color: hsla(150, 50%, 50%, 1);
+}
+#wordlist b.ELPFX {
+    background-color: hsla(150, 30%, 50%, 1);
+}
+#wordlist b.UNKNOWN {
     background-color: hsla(0, 50%, 50%, 1);
 }
-#wordlist b.nb {
+#wordlist b.NUM {
+    background-color: hsla(180, 50%, 50%, 1);
+}
+#wordlist b.COMPLEX {
+    background-color: hsla(60, 50%, 50%, 1);
+}
+#wordlist b.SEPARATOR {
     background-color: hsla(210, 50%, 50%, 1);
 }
-#wordlist b.mbok {
-    background-color: hsla(60, 50%, 50%, 1);
+#wordlist b.LINK {
+    background-color: hsla(270, 50%, 50%, 1);
 }
 #wordlist s {
     color: hsla(0, 0%, 60%, 1);
     text-decoration: none;
 }

Index: gc_lang/fr/xpi/data/lxg_panel.js
==================================================================
--- gc_lang/fr/xpi/data/lxg_panel.js
+++ gc_lang/fr/xpi/data/lxg_panel.js
@@ -25,22 +25,19 @@
     self.port.emit('openConjugueur');
 });
 */
 
 self.port.on("addSeparator", function (sText) {
-    if (document.getElementById("wordlist").innerHTML !== "") {
-        let xElem = document.createElement("p");
-        xElem.className = "separator";
-        xElem.innerHTML = sText;
-        document.getElementById("wordlist").appendChild(xElem);
-    }
+    addSeparator(sText);
+});
+
+self.port.on("addParagraphElems", function (sJSON) {
+    addParagraphElems(sJSON);
 });
 
-self.port.on("addElem", function (sHtml) {
-    let xElem = document.createElement("div");
-    xElem.innerHTML = sHtml;
-    document.getElementById("wordlist").appendChild(xElem);
+self.port.on("addMessage", function (sClass, sText) {
+    addMessage(sClass, sText);
 });
 
 self.port.on("clear", function (sHtml) {
     document.getElementById("wordlist").textContent = "";
 });
@@ -65,10 +62,71 @@
         }
     },
     false
 );
 
+
+/*
+    Actions
+*/
+
+function addSeparator (sText) {
+    if (document.getElementById("wordlist").textContent !== "") {
+        let xElem = document.createElement("p");
+        xElem.className = "separator";
+        xElem.textContent = sText;
+        document.getElementById("wordlist").appendChild(xElem);
+    }
+}
+
+function addMessage (sClass, sText) {
+    let xNode = document.createElement("p");
+    xNode.className = sClass;
+    xNode.textContent = sText;
+    document.getElementById("wordlist").appendChild(xNode);
+}
+
+function addParagraphElems (sJSON) {
+    try {
+        let xNodeDiv = document.createElement("div");
+        xNodeDiv.className = "paragraph";
+        let lElem = JSON.parse(sJSON);
+        for (let oToken of lElem) {
+            xNodeDiv.appendChild(createTokenNode(oToken));
+        }
+        document.getElementById("wordlist").appendChild(xNodeDiv);
+    }
+    catch (e) {
+        console.error("\n" + e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+        console.error(sJSON);
+    }
+}
+
+function createTokenNode (oToken) {
+    let xTokenNode = document.createElement("div");
+    xTokenNode.className = "token " + oToken.sType;
+    let xTokenValue = document.createElement("b");
+    xTokenValue.className = oToken.sType;
+    xTokenValue.textContent = oToken.sValue;
+    xTokenNode.appendChild(xTokenValue);
+    let xSep = document.createElement("s");
+    xSep.textContent = " : ";
+    xTokenNode.appendChild(xSep);
+    if (oToken.aLabel.length === 1) {
+        xTokenNode.appendChild(document.createTextNode(oToken.aLabel[0]));
+    } else {
+        let xTokenList = document.createElement("ul");
+        for (let sLabel of oToken.aLabel) {
+            let xTokenLine = document.createElement("li");
+            xTokenLine.textContent = sLabel;
+            xTokenList.appendChild(xTokenLine);
+        }
+        xTokenNode.appendChild(xTokenList);
+    }
+    return xTokenNode;
+}
+
 
 // display selection
 
 function displayClasses () {
     setHidden("ok", document.getElementById("ok").checked);

Index: gc_lang/fr/xpi/data/test_panel.js
==================================================================
--- gc_lang/fr/xpi/data/test_panel.js
+++ gc_lang/fr/xpi/data/test_panel.js
@@ -2,13 +2,13 @@
 
 /*
 	Events
 */
 
-self.port.on("addElem", function (sHtml) {
-	let xElem = document.createElement("p");
-	xElem.innerHTML = sHtml;
+self.port.on("addElem", function (sText) {
+	let xElem = document.createElement("pre");
+	xElem.textContent = sText;
 	document.getElementById("results").appendChild(xElem);
 });
 
 self.port.on("clear", function () {
 	document.getElementById("results").textContent = "";

Index: gc_lang/fr/xpi/gce_worker.js
==================================================================
--- gc_lang/fr/xpi/gce_worker.js
+++ gc_lang/fr/xpi/gce_worker.js
@@ -84,36 +84,14 @@
     return JSON.stringify(aGrammErr);
 }
 
 function parseAndSpellcheck (sText, sLang, bDebug, bContext) {
     let aGrammErr = gce.parse(sText, sLang, bDebug, bContext);
-    let aSpellErr = [];
-    for (let oToken of oTokenizer.genTokens(sText)) {
-        if (oToken.sType === 'WORD' && !oDict.isValidToken(oToken.sValue)) {
-            aSpellErr.push(oToken);
-        }
-    }
+    let aSpellErr = oTokenizer.getSpellingErrors(sText, oDict);
     return JSON.stringify({ aGrammErr: aGrammErr, aSpellErr: aSpellErr });
 }
 
-function parseAndTag (sText, iParagraph, sLang, bDebug) {
-    sText = text.addHtmlEntities(sText);
-    let aSpellErr = [];
-    for (let oToken of oTokenizer.genTokens(sText)) {
-        if (oToken.sType === 'WORD' && !oDict.isValidToken(oToken.sValue)) {
-            aSpellErr.push(oToken);
-        }
-    }
-    let aGrammErr = gce.parse(sText, sLang, bDebug);
-    let sHtml = text.tagParagraph(sText, iParagraph, aGrammErr, aSpellErr);
-    return sHtml;
-}
-
-function parseAndGenerateParagraph (sText, iParagraph, sLang, bDebug) {
-    return text.createHTMLBlock(parseAndTag(sText, iParagraph, sLang, bDebug), iParagraph);
-}
-
 function getOptions () {
     return gce.getOptions()._toString();
 }
 
 function getDefaultOptions () {
@@ -146,17 +124,30 @@
     let tests = require("resource://grammalecte/tests.js");
     let oTest = new tests.TestGrammarChecking(gce);
     let sAllRes = "";
     for (let sRes of oTest.testParse()) {
         dump(sRes+"\n");
-        sAllRes += sRes+"<br/>";
+        sAllRes += sRes+"\n";
     }
     gce.setOptions(dMemoOptions);
     return sAllRes;
 }
 
 
 // Lexicographer
 
-function analyzeWords (sText) {
-    return oLxg.analyzeText(sText);
+function getListOfElements (sText) {
+    try {
+        let aElem = [];
+        let aRes = null;
+        for (let oToken of oTokenizer.genTokens(sText)) {
+            aRes = oLxg.getInfoForToken(oToken);
+            if (aRes) {
+                aElem.push(aRes);
+            }
+        }
+        return JSON.stringify(aElem);
+    }
+    catch (e) {
+        helpers.logerror(e);
+    }
 }

Index: gc_lang/fr/xpi/package.json
==================================================================
--- gc_lang/fr/xpi/package.json
+++ gc_lang/fr/xpi/package.json
@@ -1,10 +1,10 @@
 {
   "name": "grammalecte-fr",
   "title": "Grammalecte [fr]",
   "id": "French-GC@grammalecte.net",
-  "version": "0.5.17.2",
+  "version": "0.5.18",
   "description": "Correcteur grammatical pour le français",
   "homepage": "http://www.dicollecte.org/grammalecte",
   "main": "ui.js",
   "icon": "data/img/icon-48.png",
   "scripts": {

Index: gc_lang/fr/xpi/ui.js
==================================================================
--- gc_lang/fr/xpi/ui.js
+++ gc_lang/fr/xpi/ui.js
@@ -347,14 +347,15 @@
     }
     return true;
 }
 
 function checkAndSendToPanel (sIdParagraph, sText) {
-    let xPromise = xGCEWorker.post('parseAndTag', [sText, parseInt(sIdParagraph), "FR", false]);
+    let xPromise = xGCEWorker.post('parseAndSpellcheck', [sText, "FR", false, false]);
     xPromise.then(
         function (aVal) {
-            xGCPanel.port.emit("refreshParagraph", sIdParagraph, aVal);
+            sText = text.addHtmlEntities(sText);
+            xGCPanel.port.emit("refreshParagraph", sText, sIdParagraph, aVal);
         },
         function (aReason) {
             console.error('Promise rejected - ', aReason);
         }
     ).catch(
@@ -416,20 +417,20 @@
     let sRes = "";
     try {
         sText = sText.normalize("NFC"); // remove combining diacritics
         for (let sParagraph of text.getParagraph(sText)) {
             if (sParagraph.trim() !== "") {
-                sRes = await xGCEWorker.post('parseAndGenerateParagraph', [sParagraph, iParagraph, "FR", false])
-                xGCPanel.port.emit("addElem", sRes);
+                sRes = await xGCEWorker.post('parseAndSpellcheck', [sParagraph, "FR", false, false]);
+                xGCPanel.port.emit("addParagraph", sParagraph, iParagraph, sRes);
                 nParagraph += 1;
             }
             iParagraph += 1;
         }
-        xGCPanel.port.emit("addElem", '<p class="message">' + _("numberOfParagraphs") + " " + nParagraph + '</p>');
+        xGCPanel.port.emit("addMessage", 'message', _("numberOfParagraphs") + " " + nParagraph);
     }
     catch (e) {
-        xGCPanel.port.emit("addElem", '<p class="bug">' + e.message + '</p>');
+        xGCPanel.port.emit("addMessage", 'bug', e.message);
     }
     xGCPanel.port.emit("end");
 }
 
 
@@ -569,19 +570,19 @@
     let nParagraph = 0; // non empty paragraphs
     let sRes = "";
     try {
         for (let sParagraph of text.getParagraph(sText)) {
             if (sParagraph.trim() !== "") {
-                sRes = await xGCEWorker.post('analyzeWords', [sParagraph])
-                xLxgPanel.port.emit("addElem", sRes);
+                sRes = await xGCEWorker.post('getListOfElements', [sParagraph]);
+                xLxgPanel.port.emit("addParagraphElems", sRes);
                 nParagraph += 1;
             }
         }
-        xLxgPanel.port.emit("addElem", '<p class="message">' + _("numberOfParagraphs") + " " + nParagraph + '</p>');
+        xLxgPanel.port.emit("addMessage", 'message', _("numberOfParagraphs") + " " + nParagraph);
     }
     catch (e) {
-        xLxgPanel.port.emit("addElem", '<p class="bug">'+e.message+"</p>");
+        xLxgPanel.port.emit("addMessage", 'bug', e.message);
     }
     xLxgPanel.port.emit("stopWaitIcon");
 }