Grammalecte  Check-in [af32e20bdb]

Overview
Comment:merge trunk
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | multid
Files: files | file ages | folders
SHA3-256: af32e20bdb0bcb20ac7d61d1aa9e467db9a6f13792aa1f4de86968e0cb9d2e96
User & Date: olr on 2018-02-23 21:00:52
Other Links: branch diff | manifest | tags
Context
2018-02-25
08:47
[lo] lexicon editor check-in: 5a1baacbbb user: olr tags: new_feature, lo, multid
2018-02-23
21:00
merge trunk check-in: af32e20bdb user: olr tags: multid
20:46
[lo][bug] enumerator: valuemax of progressbar check-in: 1cb2c395f6 user: olr tags: trunk, lo
2018-02-19
18:06
[lo] dictionaries options: remove print check-in: ba7c03de83 user: olr tags: lo, multid
Changes

Modified compile_rules.py from [9bd1433006] to [b3cfeb04f1].

430
431
432
433
434
435
436

437
438
439
440
441
442
443





444
445
446
447
448
449
450
    print("  parsing rules...")
    global dDEF
    lLine = []
    lRuleLine = []
    lTest = []
    lOpt = []
    zBookmark = re.compile("^!!+")


    for i, sLine in enumerate(lRules, 1):
        if sLine.startswith('#END'):
            printBookmark(0, "BREAK BY #END", i)
            break
        elif sLine.startswith("#"):
            pass





        elif sLine.startswith("DEF:"):
            m = re.match("DEF: +([a-zA-Z_][a-zA-Z_0-9]*) +(.+)$", sLine.strip())
            if m:
                dDEF["{"+m.group(1)+"}"] = m.group(2)
            else:
                print("Error in definition: ", end="")
                print(sLine.strip())







>







>
>
>
>
>







430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
    print("  parsing rules...")
    global dDEF
    lLine = []
    lRuleLine = []
    lTest = []
    lOpt = []
    zBookmark = re.compile("^!!+")
    zGraphLink = re.compile(r"^@@@@GRAPHLINK>(\w+)@@@@")

    for i, sLine in enumerate(lRules, 1):
        if sLine.startswith('#END'):
            printBookmark(0, "BREAK BY #END", i)
            break
        elif sLine.startswith("#"):
            pass
        elif sLine.startswith("@@@@"):
            m = re.match(r"^@@@@GRAPHLINK>(\w+)@@@@", sLine.strip())
            if m:
                #lRuleLine.append(["@GRAPHLINK", m.group(1)])
                printBookmark(1, "@GRAPHLINK: " + m.group(1), i)
        elif sLine.startswith("DEF:"):
            m = re.match("DEF: +([a-zA-Z_][a-zA-Z_0-9]*) +(.+)$", sLine.strip())
            if m:
                dDEF["{"+m.group(1)+"}"] = m.group(2)
            else:
                print("Error in definition: ", end="")
                print(sLine.strip())

Modified gc_core/py/__init__.py from [a7ffc6f8bf] to [aeadedff14].





>
>
1
2

from .grammar_checker import *

Added gc_core/py/grammar_checker.py version [79ce1061e8].



















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# Grammalecte
# Main class: wrapper

import importlib
import json

from . import text


class GrammarChecker:

    def __init__ (self, sLangCode, sContext="Python"):
        self.sLangCode = sLangCode
        # Grammar checker engine
        self.gce = importlib.import_module("."+sLangCode, "grammalecte")
        self.gce.load(sContext)
        # Spell checker
        self.oSpellChecker = self.gce.getSpellChecker()
        # Lexicographer
        self.oLexicographer = None
        # Text formatter
        self.oTextFormatter = None

    def getGCEngine (self):
        return self.gce

    def getSpellChecker (self):
        return self.oSpellChecker

    def getTextFormatter (self):
        if self.oTextFormatter == None:
            self.tf = importlib.import_module("."+self.sLangCode+".textformatter", "grammalecte")
        self.oTextFormatter = self.tf.TextFormatter()
        return self.oTextFormatter

    def getLexicographer (self):
        if self.oLexicographer == None:
            self.lxg = importlib.import_module("."+self.sLangCode+".lexicographe", "grammalecte")
        self.oLexicographer = self.lxg.Lexicographe(self.oSpellChecker)
        return self.oLexicographer

    def displayGCOptions (self):
        self.gce.displayOptions()

    def getParagraphErrors (self, sText, dOptions=None, bContext=False, bSpellSugg=False, bDebug=False):
        "returns a tuple: (grammar errors, spelling errors)"
        aGrammErrs = self.gce.parse(sText, "FR", bDebug=bDebug, dOptions=dOptions, bContext=bContext)
        aSpellErrs = self.oSpellChecker.parseParagraph(sText, bSpellSugg)
        return aGrammErrs, aSpellErrs

    def generateText (self, sText, bEmptyIfNoErrors=False, bSpellSugg=False, nWidth=100, bDebug=False):
        pass

    def generateTextAsJSON (self, sText, bContext=False, bEmptyIfNoErrors=False, bSpellSugg=False, bReturnText=False, bDebug=False):
        pass

    def generateParagraph (self, sText, dOptions=None, bEmptyIfNoErrors=False, bSpellSugg=False, nWidth=100, bDebug=False):
        aGrammErrs, aSpellErrs = self.getParagraphErrors(sText, dOptions, False, bSpellSugg, bDebug)
        if bEmptyIfNoErrors and not aGrammErrs and not aSpellErrs:
            return ""
        return text.generateParagraph(sText, aGrammErrs, aSpellErrs, nWidth)

    def generateParagraphAsJSON (self, iIndex, sText, dOptions=None, bContext=False, bEmptyIfNoErrors=False, bSpellSugg=False, bReturnText=False, lLineSet=None, bDebug=False):
        aGrammErrs, aSpellErrs = self.getParagraphErrors(sText, dOptions, bContext, bSpellSugg, bDebug)
        aGrammErrs = list(aGrammErrs)
        if bEmptyIfNoErrors and not aGrammErrs and not aSpellErrs:
            return ""
        if lLineSet:
            aGrammErrs, aSpellErrs = text.convertToXY(aGrammErrs, aSpellErrs, lLineSet)
            return json.dumps({ "lGrammarErrors": aGrammErrs, "lSpellingErrors": aSpellErrs }, ensure_ascii=False)
        if bReturnText:
            return json.dumps({ "iParagraph": iIndex, "sText": sText, "lGrammarErrors": aGrammErrs, "lSpellingErrors": aSpellErrs }, ensure_ascii=False)
        return json.dumps({ "iParagraph": iIndex, "lGrammarErrors": aGrammErrs, "lSpellingErrors": aSpellErrs }, ensure_ascii=False)

Modified gc_lang/fr/config.ini from [576d6e6c29] to [87fa24eb7e].

90
91
92
93
94
95
96



97
98
99
100
101
oxt/ContextMenu/ContextMenu.py = ContextMenu.py
oxt/ContextMenu/jobs.xcu = config/jobs.xcu
# TextFormatter
oxt/TextFormatter/TextFormatter.py = pythonpath/TextFormatter.py
oxt/TextFormatter/tf_strings.py = pythonpath/tf_strings.py
oxt/TextFormatter/tf_options.py = pythonpath/tf_options.py
oxt/TextFormatter/tf_tabrep.py = pythonpath/tf_tabrep.py



# Conjugueur
oxt/Conjugueur/Conjugueur.py = pythonpath/Conjugueur.py
# Modify author
oxt/ChangeAuthor/Author.py = pythonpath/Author.py
oxt/ChangeAuthor/ca_strings.py = pythonpath/ca_strings.py







>
>
>





90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
oxt/ContextMenu/ContextMenu.py = ContextMenu.py
oxt/ContextMenu/jobs.xcu = config/jobs.xcu
# TextFormatter
oxt/TextFormatter/TextFormatter.py = pythonpath/TextFormatter.py
oxt/TextFormatter/tf_strings.py = pythonpath/tf_strings.py
oxt/TextFormatter/tf_options.py = pythonpath/tf_options.py
oxt/TextFormatter/tf_tabrep.py = pythonpath/tf_tabrep.py
# Lexicographer
oxt/Lexicographer/Enumerator.py = pythonpath/Enumerator.py
oxt/Lexicographer/enum_strings.py = pythonpath/enum_strings.py
# Conjugueur
oxt/Conjugueur/Conjugueur.py = pythonpath/Conjugueur.py
# Modify author
oxt/ChangeAuthor/Author.py = pythonpath/Author.py
oxt/ChangeAuthor/ca_strings.py = pythonpath/ca_strings.py

Modified gc_lang/fr/oxt/AppLauncher.py from [851c2d6eab] to [91f766cacd].

54
55
56
57
58
59
60




61
62
63
64
65
66
67
                import Author
                xDialog = Author.Author(self.ctx)
                xDialog.run(self.sLang)
            elif sCmd == "OP":
                import Options
                xDialog = Options.GC_Options(self.ctx)
                xDialog.run(self.sLang)




            elif sCmd.startswith("FA/"):
                findAll(sCmd[6:], (sCmd[3:4] == "y"), (sCmd[4:5] == "y"))
            # elif sCmd.startswith("URL/"):
            #     # Call from context menu to launch URL?
            #     # http://opengrok.libreoffice.org/xref/core/sw/source/ui/lingu/olmenu.cxx#785
            #     xSystemShellExecute = self.ctx.getServiceManager().createInstanceWithContext('com.sun.star.system.SystemShellExecute', self.ctx)
            #     xSystemShellExecute.execute(url, "", uno.getConstantByName("com.sun.star.system.SystemShellExecuteFlags.URIS_ONLY"))







>
>
>
>







54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
                import Author
                xDialog = Author.Author(self.ctx)
                xDialog.run(self.sLang)
            elif sCmd == "OP":
                import Options
                xDialog = Options.GC_Options(self.ctx)
                xDialog.run(self.sLang)
            elif sCmd == "EN":
                import Enumerator
                xDialog = Enumerator.Enumerator(self.ctx)
                xDialog.run(self.sLang)
            elif sCmd.startswith("FA/"):
                findAll(sCmd[6:], (sCmd[3:4] == "y"), (sCmd[4:5] == "y"))
            # elif sCmd.startswith("URL/"):
            #     # Call from context menu to launch URL?
            #     # http://opengrok.libreoffice.org/xref/core/sw/source/ui/lingu/olmenu.cxx#785
            #     xSystemShellExecute = self.ctx.getServiceManager().createInstanceWithContext('com.sun.star.system.SystemShellExecute', self.ctx)
            #     xSystemShellExecute.execute(url, "", uno.getConstantByName("com.sun.star.system.SystemShellExecuteFlags.URIS_ONLY"))

Added gc_lang/fr/oxt/Lexicographer/Enumerator.py version [753041f6b5].

































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# Dictionary Options
# by Olivier R.
# License: MPL 2

import unohelper
import uno
import traceback

import helpers
import enum_strings
import grammalecte.graphspell as sc

from com.sun.star.task import XJobExecutor
from com.sun.star.awt import XActionListener
from com.sun.star.beans import PropertyValue


def hexToRBG (sHexa):
    r = int(sHexa[:2], 16)
    g = int(sHexa[2:4], 16)
    b = int(sHexa[4:], 16)
    return (r & 255) << 16 | (g & 255) << 8 | (b & 255)


def _waitPointer (funcDecorated):
    def wrapper (*args, **kwargs):
        # self is the first parameter if the decorator is applied on a object
        self = args[0]
        # before
        xPointer = self.xSvMgr.createInstanceWithContext("com.sun.star.awt.Pointer", self.ctx)
        xPointer.setType(uno.getConstantByName("com.sun.star.awt.SystemPointer.WAIT"))
        xWindowPeer = self.xContainer.getPeer()
        xWindowPeer.setPointer(xPointer)
        for x in xWindowPeer.Windows:
            x.setPointer(xPointer)
        # processing
        result = funcDecorated(*args, **kwargs)
        # after
        xPointer.setType(uno.getConstantByName("com.sun.star.awt.SystemPointer.ARROW"))
        xWindowPeer.setPointer(xPointer)
        for x in xWindowPeer.Windows:
            x.setPointer(xPointer)
        self.xContainer.setVisible(True) # seems necessary to refresh the dialog box and text widgets (why?)
        # return
        return result
    return wrapper


class Enumerator (unohelper.Base, XActionListener, XJobExecutor):

    def __init__ (self, ctx):
        self.ctx = ctx
        self.xSvMgr = self.ctx.ServiceManager
        self.xDesktop = self.xSvMgr.createInstanceWithContext("com.sun.star.frame.Desktop", self.ctx)
        self.xDocument = self.xDesktop.getCurrentComponent()
        self.xContainer = None
        self.xDialog = None
        self.oSpellChecker = None

    def _addWidget (self, name, wtype, x, y, w, h, **kwargs):
        xWidget = self.xDialog.createInstance('com.sun.star.awt.UnoControl%sModel' % wtype)
        xWidget.Name = name
        xWidget.PositionX = x
        xWidget.PositionY = y
        xWidget.Width = w
        xWidget.Height = h
        for k, w in kwargs.items():
            setattr(xWidget, k, w)
        self.xDialog.insertByName(name, xWidget)
        return xWidget

    def _addGrid (self, name, x, y, w, h, columns, **kwargs):
        xGridModel = self.xDialog.createInstance('com.sun.star.awt.grid.UnoControlGridModel')
        xGridModel.Name = name
        xGridModel.PositionX = x
        xGridModel.PositionY = y
        xGridModel.Width = w
        xGridModel.Height = h
        xColumnModel = xGridModel.ColumnModel
        for e in columns:
            xCol = xColumnModel.createColumn()
            for k, w in e.items():
                setattr(xCol, k, w)
            xColumnModel.addColumn(xCol)
        for k, w in kwargs.items():
            setattr(xGridModel, k, w)
        self.xDialog.insertByName(name, xGridModel)
        return xGridModel

    def run (self, sLang):
        self.dUI = enum_strings.getUI(sLang)

        # dialog
        self.xDialog = self.xSvMgr.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', self.ctx)
        self.xDialog.Width = 240
        self.xDialog.Height = 280
        self.xDialog.Title = self.dUI.get('title', "#title#")
        xWindowSize = helpers.getWindowSize()
        self.xDialog.PositionX = int((xWindowSize.Width / 2) - (self.xDialog.Width / 2))
        self.xDialog.PositionY = int((xWindowSize.Height / 2) - (self.xDialog.Height / 2))

        # fonts
        xFDTitle = uno.createUnoStruct("com.sun.star.awt.FontDescriptor")
        xFDTitle.Height = 9
        xFDTitle.Weight = uno.getConstantByName("com.sun.star.awt.FontWeight.BOLD")
        xFDTitle.Name = "Verdana"
        
        xFDSubTitle = uno.createUnoStruct("com.sun.star.awt.FontDescriptor")
        xFDSubTitle.Height = 8
        xFDSubTitle.Weight = uno.getConstantByName("com.sun.star.awt.FontWeight.BOLD")
        xFDSubTitle.Name = "Verdana"

        # widget
        nX = 10
        nY1 = 5
        nY2 = nY1 + 225

        nWidth = self.xDialog.Width - 20
        nHeight = 10

        # List
        self._addWidget("list_section", 'FixedLine', nX, nY1, nWidth, nHeight, Label = self.dUI.get("list_section", "#err"), FontDescriptor = xFDTitle)
        self._addWidget('count_button', 'Button', nX, nY1+12, 70, 10, Label = self.dUI.get('count_button', "#err"))
        self._addWidget('count2_button', 'Button', nX+75, nY1+12, 70, 10, Label = self.dUI.get('count2_button', "#err"))
        self._addWidget('unknown_button', 'Button', nX+150, nY1+12, 70, 10, Label = self.dUI.get('unknown_button', "#err"))
        self.xGridModel = self._addGrid("list_grid", nX, nY1+25, nWidth, 180, [
            {"Title": self.dUI.get("words", "#err"), "ColumnWidth": 175},
            {"Title": "Occurrences", "ColumnWidth": 45}
        ])
        self._addWidget('num_of_entries', 'FixedText', nX, nY1+210, 60, nHeight, Label = self.dUI.get('num_of_entries', "#err"), Align = 2)
        self.xNumWord = self._addWidget('num_of_entries_res', 'FixedText', nX+65, nY1+210, 30, nHeight, Label = "—")
        self._addWidget('tot_of_entries', 'FixedText', nX+100, nY1+210, 60, nHeight, Label = self.dUI.get('tot_of_entries', "#err"), Align = 2)
        self.xTotWord = self._addWidget('tot_of_entries_res', 'FixedText', nX+165, nY1+210, 30, nHeight, Label = "—")
        
        # Tag
        # Note: the only way to group RadioButtons is to create them successively
        self._addWidget("dformat_section", 'FixedLine', nX, nY2, 90, nHeight, Label = self.dUI.get("dformat_section", "#err"), FontDescriptor = xFDTitle)
        self._addWidget("charstyle_section", 'FixedLine', nX+100, nY2, 90, nHeight, Label = self.dUI.get("charstyle_section", "#err"), FontDescriptor = xFDTitle)
        self.xUnderline = self._addWidget('underline', 'RadioButton', nX, nY2+12, 40, nHeight, Label = self.dUI.get('underline', "#err"))
        self.xNoUnderline = self._addWidget('nounderline', 'RadioButton', nX+50, nY2+12, 40, nHeight, Label = self.dUI.get('nounderline', "#err"))
        self.xAccent = self._addWidget('accentuation', 'RadioButton', nX+100, nY2+12, 50, nHeight, Label = self.dUI.get('accentuation', "#err"))
        self.xNoAccent = self._addWidget('noaccentuation', 'RadioButton', nX+155, nY2+12, 40, nHeight, Label = self.dUI.get('noaccentuation', "#err"))

        self.xTag = self._addWidget('tag_button', 'Button', self.xDialog.Width-40, nY2+10, 30, 11, Label = self.dUI.get('tag_button', "#err"), FontDescriptor = xFDTitle, TextColor = 0x005500)
        
        # Progress bar
        self.xProgressBar = self._addWidget('progress_bar', 'ProgressBar', nX, self.xDialog.Height-25, 160, 14)
        self.xProgressBar.ProgressValueMin = 0
        self.xProgressBar.ProgressValueMax = 1 # to calculate

        # Close
        self._addWidget('close_button', 'Button', self.xDialog.Width-60, self.xDialog.Height-25, 50, 14, Label = self.dUI.get('close_button', "#err"), FontDescriptor = xFDTitle, TextColor = 0x550000)

        # container
        self.xContainer = self.xSvMgr.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', self.ctx)
        self.xContainer.setModel(self.xDialog)
        self.xGridControl = self.xContainer.getControl('list_grid')
        self.xContainer.getControl('count_button').addActionListener(self)
        self.xContainer.getControl('count_button').setActionCommand('Count')
        self.xContainer.getControl('count2_button').addActionListener(self)
        self.xContainer.getControl('count2_button').setActionCommand('CountByLemma')
        self.xContainer.getControl('unknown_button').addActionListener(self)
        self.xContainer.getControl('unknown_button').setActionCommand('UnknownWords')
        self.xContainer.getControl('tag_button').addActionListener(self)
        self.xContainer.getControl('tag_button').setActionCommand('Tag')
        self.xContainer.getControl('close_button').addActionListener(self)
        self.xContainer.getControl('close_button').setActionCommand('Close')
        self.xContainer.setVisible(False)
        xToolkit = self.xSvMgr.createInstanceWithContext('com.sun.star.awt.ExtToolkit', self.ctx)
        self.xContainer.createPeer(xToolkit, None)
        self.xContainer.execute()

    # XActionListener
    def actionPerformed (self, xActionEvent):
        try:
            if xActionEvent.ActionCommand == "Count":
                self.count(self.dUI.get("words", "#err"))
                self.xTag.Enabled = True
            elif xActionEvent.ActionCommand == "CountByLemma":
                self.count(self.dUI.get("lemmas", "#err"), bByLemma=True)
                self.xTag.Enabled = False
            elif xActionEvent.ActionCommand == "UnknownWords":
                self.count(self.dUI.get("unknown_words", "#err"), bOnlyUnknownWords=True)
                self.xTag.Enabled = True
            elif xActionEvent.ActionCommand == "Tag":
                nRow = self.xGridControl.getCurrentRow()
                if nRow == -1:
                    return
                sWord = self.xGridModel.GridDataModel.getCellData(0, nRow)
                if not sWord:
                    return
                sAction = ""
                if self.xUnderline.State:
                    sAction = "underline"
                elif self.xNoUnderline.State:
                    sAction = "nounderline"
                elif self.xAccent.State:
                    sAction = "accentuation"
                elif self.xNoAccent.State:
                    sAction = "noaccentuation"
                self.tagText(sWord, sAction)
            elif xActionEvent.ActionCommand == "Close":
                self.xContainer.endExecute()
        except:
            traceback.print_exc()
    
    # XJobExecutor
    def trigger (self, args):
        try:
            xDialog = Enumerator(self.ctx)
            xDialog.run()
        except:
            traceback.print_exc()

    # Code
    def _setTitleOfFirstColumn (self, sTitle):
        xColumnModel = self.xGridModel.ColumnModel
        xColumn = xColumnModel.getColumn(0)
        xColumn.Title = sTitle

    def _getParagraphsFromText (self):
        "generator: returns full document text paragraph by paragraph"
        xCursor = self.xDocument.Text.createTextCursor()
        xCursor.gotoStart(False)
        xCursor.gotoEndOfParagraph(True)
        yield xCursor.getString()
        while xCursor.gotoNextParagraph(False):
            xCursor.gotoEndOfParagraph(True)
            yield xCursor.getString()

    def _countParagraph (self):
        i = 1
        xCursor = self.xDocument.Text.createTextCursor()
        xCursor.gotoStart(False)
        while xCursor.gotoNextParagraph(False):
            i += 1
        return i

    @_waitPointer
    def count (self, sTitle, bByLemma=False, bOnlyUnknownWords=False):
        if not self.oSpellChecker:
            self.oSpellChecker = sc.SpellChecker("fr")
        self._setTitleOfFirstColumn(sTitle)
        self.xProgressBar.ProgressValueMax = self._countParagraph() * 2
        self.xProgressBar.ProgressValue = 0
        xGridDataModel = self.xGridModel.GridDataModel
        xGridDataModel.removeAllRows()
        dWord = {}
        for sParagraph in self._getParagraphsFromText():
            dWord = self.oSpellChecker.countWordsOccurrences(sParagraph, bByLemma, bOnlyUnknownWords, dWord)
            self.xProgressBar.ProgressValue += 1
        self.xProgressBar.ProgressValueMax += len(dWord)
        i = 0
        nTotOccur = 0
        for k, w in sorted(dWord.items(), key=lambda t: t[1], reverse=True):
            xGridDataModel.addRow(i, (k, w))
            self.xProgressBar.ProgressValue += 1
            i += 1
            nTotOccur += w
        self.xProgressBar.ProgressValue = self.xProgressBar.ProgressValueMax
        self.xNumWord.Label = str(i)
        self.xTotWord.Label = nTotOccur

    @_waitPointer
    def tagText (self, sWord, sAction=""):
        if not sAction:
            return
        self.xProgressBar.ProgressValueMax = self._countParagraph()
        self.xProgressBar.ProgressValue = 0
        xCursor = self.xDocument.Text.createTextCursor()
        #helpers.xray(xCursor)
        xCursor.gotoStart(False)
        xCursor.gotoEndOfParagraph(True)
        sParagraph = xCursor.getString()
        if sWord in sParagraph:
            self._tagParagraph(sWord, xCursor, sAction)
        self.xProgressBar.ProgressValue += 1
        while xCursor.gotoNextParagraph(False):
            xCursor.gotoEndOfParagraph(True)
            sParagraph = xCursor.getString()
            if sWord in sParagraph:
                self._tagParagraph(sWord, xCursor, sAction)
            self.xProgressBar.ProgressValue += 1
        self.xProgressBar.ProgressValue = self.xProgressBar.ProgressValueMax

    def _tagParagraph (self, sWord, xCursor, sAction):
        xCursor.gotoStartOfParagraph(False)
        while xCursor.gotoNextWord(False):
            if xCursor.isStartOfWord():
                xCursor.gotoEndOfWord(True)
                if sWord == xCursor.getString():
                    if sAction == "underline":
                        xCursor.CharBackColor = hexToRBG("AA0000")
                    elif sAction == "nounderline":
                        xCursor.CharBackColor = hexToRBG("FFFFFF")
                    elif sAction == "accentuation":
                        xCursor.CharStyleName = "Emphasis"
                    elif sAction == "noaccentuation":
                        #xCursor.CharStyleName = "Default Style"     # doesn’t work
                        xCursor.setPropertyToDefault("CharStyleName")


#g_ImplementationHelper = unohelper.ImplementationHelper()
#g_ImplementationHelper.addImplementation(Enumerator, 'net.grammalecte.enumerator', ('com.sun.star.task.Job',))

Added gc_lang/fr/oxt/Lexicographer/enum_strings.py version [fd05286777].















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def getUI (sLang):
    if sLang in dStrings:
        return dStrings[sLang]
    return dStrings["fr"]

dStrings = {
    "fr": {
        "title": "Grammalecte · Recenseur de mots",
        
        "list_section": "Calcul des occurrences des mots",
        "count_button": "Compter tout",
        "count2_button": "Compter par lemme",
        "unknown_button": "Mots inconnus",
        "num_of_entries": "Nombre d’entrées :",
        "tot_of_entries": "Total des entrées :",

        "words": "Mots",
        "lemmas": "Lemmes",
        "unknown_words": "Mots inconnus",

        "dformat_section": "Formatage direct",
        "charstyle_section": "Style de caractères",
        "underline": "Surligner",
        "nounderline": "Effacer",
        "accentuation": "Accentuation",
        "noaccentuation": "Aucun",
        "tag_button": "Taguer",

        "close_button": "Fermer",
    },
    "en": {
        "title": "Grammalecte · Enumerator of Words",

        "list_section": "Words",
        "count_button": "Count all",
        "count2_button": "Count by lemma",
        "unknown_button": "Unknown words",
        "num_of_entries": "Number of entries:",
        "tot_of_entries": "Total of words:",

        "words": "Words",
        "lemmas": "Lemmas",
        "unknown_words": "Unknown words",

        "dformat_section": "Direct format",
        "charstyle_section": "Character style",
        "underline": "Underline",
        "nounderline": "Erase",
        "accentuation": "Accentuation",
        "noaccentuation": "None",
        "tag_button": "Tag",

        "close_button": "Close",
    },
}

Modified gc_lang/fr/oxt/addons.xcu from [3b803f02e0] to [9b5b4f4c15].

67
68
69
70
71
72
73
















74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
                        <value>_self</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                </node>
                <node oor:name="m3" oor:op="replace">
















                    <!--<prop oor:name="URL" oor:type="xs:string">
                        <value>vnd.sun.star.script:basiclib.Module1.EditAuthorField?language=Basic&amp;location=application</value>
                    </prop>-->
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?MA</value>
                    </prop>
                    <prop oor:name="Title" oor:type="xs:string">
                        <value/>
                        <value xml:lang="fr">~Modifier le champ “Auteur”…</value>
                        <value xml:lang="en-US">~Modify the field “Author”…</value>
                    </prop>
                    <prop oor:name="Target" oor:type="xs:string">
                        <value>_self</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument</value>
                    </prop>
                </node>
                <node oor:name="m4" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>private:separator</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                </node>
                <node oor:name="m5" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?OP</value>
                    </prop>
                    <!--<prop oor:name="URL" oor:type="xs:string">
                        <value>.uno:OptionsTreeDialog?OptionsPageURL:string=%origin%/dialog/fr.xdl</value>
                    </prop>-->
                    <prop oor:name="Title" oor:type="xs:string">







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


















|







|







67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
                        <value>_self</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                </node>
                <node oor:name="m3" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?EN</value>
                    </prop>
                    <prop oor:name="Title" oor:type="xs:string">
                        <value/>
                        <value xml:lang="fr">~Recenseur de mots…</value>
                        <value xml:lang="en-US">~Enumerator of words…</value>
                    </prop>
                    <prop oor:name="Target" oor:type="xs:string">
                        <value>_self</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                </node>
                <node oor:name="m4" oor:op="replace">
                    <!--<prop oor:name="URL" oor:type="xs:string">
                        <value>vnd.sun.star.script:basiclib.Module1.EditAuthorField?language=Basic&amp;location=application</value>
                    </prop>-->
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?MA</value>
                    </prop>
                    <prop oor:name="Title" oor:type="xs:string">
                        <value/>
                        <value xml:lang="fr">~Modifier le champ “Auteur”…</value>
                        <value xml:lang="en-US">~Modify the field “Author”…</value>
                    </prop>
                    <prop oor:name="Target" oor:type="xs:string">
                        <value>_self</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument</value>
                    </prop>
                </node>
                <node oor:name="m5" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>private:separator</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                </node>
                <node oor:name="m6" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?OP</value>
                    </prop>
                    <!--<prop oor:name="URL" oor:type="xs:string">
                        <value>.uno:OptionsTreeDialog?OptionsPageURL:string=%origin%/dialog/fr.xdl</value>
                    </prop>-->
                    <prop oor:name="Title" oor:type="xs:string">
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument</value>
                    </prop>
                    <prop oor:name="ImageIdentifier" oor:type="xs:string">
                        <value>org.dicollecte.images:Grammalecte</value>
                    </prop>
                </node>
                <node oor:name="m6" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?DI</value>
                    </prop>
                    <prop oor:name="Title" oor:type="xs:string">
                        <value/>
                        <value xml:lang="fr">~Options des dictionnaires…</value>
                        <value xml:lang="en-US">Dictionaries ~options…</value>
                    </prop>
                    <prop oor:name="Target" oor:type="xs:string">
                        <value>_self</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                    <prop oor:name="ImageIdentifier" oor:type="xs:string">
                        <value>org.dicollecte.images:Frenchflag</value>
                    </prop>
                </node>
                <node oor:name="m7" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?DS</value>
                    </prop>
                    <prop oor:name="Title" oor:type="xs:string">
                        <value/>
                        <value xml:lang="fr">~Options orthographiques…</value>
                        <value xml:lang="en-US">Spelling ~options…</value>
                    </prop>
                    <prop oor:name="Target" oor:type="xs:string">
                        <value>_self</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                    <prop oor:name="ImageIdentifier" oor:type="xs:string">
                        <value>org.dicollecte.images:Frenchflag</value>
                    </prop>
                </node>
                <node oor:name="m8" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>private:separator</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                </node>
                <node oor:name="m9" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?About</value>
                    </prop>
                    <prop oor:name="Title" oor:type="xs:string">
                        <value/>
                        <value xml:lang="fr">À ~propos de Grammalecte…</value>
                        <value xml:lang="en-US">~About Grammalecte…</value>







|


















|


















|







|







131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument</value>
                    </prop>
                    <prop oor:name="ImageIdentifier" oor:type="xs:string">
                        <value>org.dicollecte.images:Grammalecte</value>
                    </prop>
                </node>
                <node oor:name="m7" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?DI</value>
                    </prop>
                    <prop oor:name="Title" oor:type="xs:string">
                        <value/>
                        <value xml:lang="fr">~Options des dictionnaires…</value>
                        <value xml:lang="en-US">Dictionaries ~options…</value>
                    </prop>
                    <prop oor:name="Target" oor:type="xs:string">
                        <value>_self</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                    <prop oor:name="ImageIdentifier" oor:type="xs:string">
                        <value>org.dicollecte.images:Frenchflag</value>
                    </prop>
                </node>
                <node oor:name="m8" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?DS</value>
                    </prop>
                    <prop oor:name="Title" oor:type="xs:string">
                        <value/>
                        <value xml:lang="fr">~Options orthographiques…</value>
                        <value xml:lang="en-US">Spelling ~options…</value>
                    </prop>
                    <prop oor:name="Target" oor:type="xs:string">
                        <value>_self</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                    <prop oor:name="ImageIdentifier" oor:type="xs:string">
                        <value>org.dicollecte.images:Frenchflag</value>
                    </prop>
                </node>
                <node oor:name="m9" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>private:separator</value>
                    </prop>
                    <prop oor:name="Context" oor:type="xs:string">
                        <value>com.sun.star.text.TextDocument,com.sun.star.text.GlobalDocument,com.sun.star.text.WebDocument,com.sun.star.presentation.PresentationDocument</value>
                    </prop>
                </node>
                <node oor:name="m10" oor:op="replace">
                    <prop oor:name="URL" oor:type="xs:string">
                        <value>service:net.grammalecte.AppLauncher?About</value>
                    </prop>
                    <prop oor:name="Title" oor:type="xs:string">
                        <value/>
                        <value xml:lang="fr">À ~propos de Grammalecte…</value>
                        <value xml:lang="en-US">~About Grammalecte…</value>

Modified gc_lang/fr/perf_memo.txt from [15962af16c] to [b880c3770c].

19
20
21
22
23
24
25
26
0.5.12      2016.10.14 18:58    4.51895     1.0843      0.772805    0.22387     0.249411    0.261593    0.628802    0.339303    0.0570326   0.00805416  
0.5.15      2017.01.22 11:44    4.85204     1.16134     0.770762    0.227874    0.244574    0.253305    0.58831     0.319987    0.0603996   0.00694786  
0.5.15      2017.01.22 11:47    4.85593     1.15248     0.762924    0.22744     0.243461    0.254609    0.586741    0.317503    0.0588827   0.00701016  (unicode normalisation NFC)
0.5.15      2017.01.31 12:06    4.88227     1.18008     0.782217    0.232617    0.247672    0.257628    0.596903    0.32169     0.0603505   0.00695196  
0.5.15      2017.02.05 10:10    4.90222     1.18444     0.786696    0.233413    0.25071     0.260214    0.602112    0.325235    0.0609932   0.00706897  
0.5.16      2017.05.12 07:41    4.92201     1.19269     0.80639     0.239147    0.257518    0.266523    0.62111     0.33359     0.0634668   0.00757178  
0.6.1       2018.02.12 09:58    5.25924     1.2649      0.878442    0.257465    0.280558    0.293903    0.686887    0.391275    0.0672474   0.00824723  
0.6.2       2018.02.19 09:06    6.20116     1.44334     1.02936     0.272956    0.311561    0.362367    0.812705    0.419061    0.0773003   0.00845671  (spelling normalization)







|
19
20
21
22
23
24
25
26
0.5.12      2016.10.14 18:58    4.51895     1.0843      0.772805    0.22387     0.249411    0.261593    0.628802    0.339303    0.0570326   0.00805416  
0.5.15      2017.01.22 11:44    4.85204     1.16134     0.770762    0.227874    0.244574    0.253305    0.58831     0.319987    0.0603996   0.00694786  
0.5.15      2017.01.22 11:47    4.85593     1.15248     0.762924    0.22744     0.243461    0.254609    0.586741    0.317503    0.0588827   0.00701016  (unicode normalisation NFC)
0.5.15      2017.01.31 12:06    4.88227     1.18008     0.782217    0.232617    0.247672    0.257628    0.596903    0.32169     0.0603505   0.00695196  
0.5.15      2017.02.05 10:10    4.90222     1.18444     0.786696    0.233413    0.25071     0.260214    0.602112    0.325235    0.0609932   0.00706897  
0.5.16      2017.05.12 07:41    4.92201     1.19269     0.80639     0.239147    0.257518    0.266523    0.62111     0.33359     0.0634668   0.00757178  
0.6.1       2018.02.12 09:58    5.25924     1.2649      0.878442    0.257465    0.280558    0.293903    0.686887    0.391275    0.0672474   0.00824723  
0.6.2       2018.02.19 19:06    5.51302     1.29359     0.874157    0.260415    0.271596    0.290641    0.684754    0.376905    0.0815201   0.00919633  (spelling normalization)

Modified gc_lang/fr/webext/gce_worker.js from [c20f81d8f3] to [efd11a103b].

200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
}

function parseAndSpellcheck (sText, sCountry, bDebug, bContext, dInfo={}) {
    let i = 0;
    sText = sText.replace(/­/g, "").normalize("NFC");
    for (let sParagraph of text.getParagraph(sText)) {
        let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, bContext);
        let aSpellErr = oTokenizer.getSpellingErrors(sParagraph, oSpellChecker);
        postMessage(createResponse("parseAndSpellcheck", {sParagraph: sParagraph, iParaNum: i, aGrammErr: aGrammErr, aSpellErr: aSpellErr}, dInfo, false));
        i += 1;
    }
    postMessage(createResponse("parseAndSpellcheck", null, dInfo, true));
}

function parseAndSpellcheck1 (sParagraph, sCountry, bDebug, bContext, dInfo={}) {
    sParagraph = sParagraph.replace(/­/g, "").normalize("NFC");
    let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, bContext);
    let aSpellErr = oTokenizer.getSpellingErrors(sParagraph, oSpellChecker);
    postMessage(createResponse("parseAndSpellcheck1", {sParagraph: sParagraph, aGrammErr: aGrammErr, aSpellErr: aSpellErr}, dInfo, true));
}

function getOptions (dInfo={}) {
    postMessage(createResponse("getOptions", gc_engine.getOptions(), dInfo, true));
}








|









|







200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
}

function parseAndSpellcheck (sText, sCountry, bDebug, bContext, dInfo={}) {
    let i = 0;
    sText = sText.replace(/­/g, "").normalize("NFC");
    for (let sParagraph of text.getParagraph(sText)) {
        let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, bContext);
        let aSpellErr = oSpellChecker.parseParagraph(sParagraph);
        postMessage(createResponse("parseAndSpellcheck", {sParagraph: sParagraph, iParaNum: i, aGrammErr: aGrammErr, aSpellErr: aSpellErr}, dInfo, false));
        i += 1;
    }
    postMessage(createResponse("parseAndSpellcheck", null, dInfo, true));
}

function parseAndSpellcheck1 (sParagraph, sCountry, bDebug, bContext, dInfo={}) {
    sParagraph = sParagraph.replace(/­/g, "").normalize("NFC");
    let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, bContext);
    let aSpellErr = oSpellChecker.parseParagraph(sParagraph);
    postMessage(createResponse("parseAndSpellcheck1", {sParagraph: sParagraph, aGrammErr: aGrammErr, aSpellErr: aSpellErr}, dInfo, true));
}

function getOptions (dInfo={}) {
    postMessage(createResponse("getOptions", gc_engine.getOptions(), dInfo, true));
}

Modified grammalecte-cli.py from [d66b04b565] to [150bfbed66].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3

import sys
import os.path
import argparse
import json

import grammalecte.fr as gce
import grammalecte.fr.lexicographe as lxg
import grammalecte.fr.textformatter as tf
import grammalecte.text as txt
import grammalecte.graphspell.tokenizer as tkz
from grammalecte.graphspell.echo import echo


_EXAMPLE = "Quoi ? Racontes ! Racontes-moi ! Bon sangg, parles ! Oui. Il y a des menteur partout. " \
           "Je suit sidéré par la brutales arrogance de cette homme-là. Quelle salopard ! Un escrocs de la pire espece. " \
           "Quant sera t’il châtiés pour ses mensonge ?             Merde ! J’en aie marre."








<
|
<

<







1
2
3
4
5
6
7

8

9

10
11
12
13
14
15
16
#!/usr/bin/env python3

import sys
import os.path
import argparse
import json


import grammalecte

import grammalecte.text as txt

from grammalecte.graphspell.echo import echo


_EXAMPLE = "Quoi ? Racontes ! Racontes-moi ! Bon sangg, parles ! Oui. Il y a des menteur partout. " \
           "Je suit sidéré par la brutales arrogance de cette homme-là. Quelle salopard ! Un escrocs de la pire espece. " \
           "Quant sera t’il châtiés pour ses mensonge ?             Merde ! J’en aie marre."

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92




93

94
95
96
97

98
99

100

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131




132
133
134
135
136
137
138
139
140

141
142
143
144
145
146
147

148
149
150
151
152
153
154
155
156

157
158


159

160
161
162
163
164
165

166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184

185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
    if sys.platform == "win32":
        # Apparently, the console transforms «’» in «'».
        # So we reverse it to avoid many useless warnings.
        sText = sText.replace("'", "’")
    return sText


def _getErrors (sText, oTokenizer, oSpellChecker, bContext=False, bSpellSugg=False, bDebug=False):
    "returns a tuple: (grammar errors, spelling errors)"
    aGrammErrs = gce.parse(sText, "FR", bDebug=bDebug, bContext=bContext)
    aSpellErrs = []
    for dToken in oTokenizer.genTokens(sText):
        if dToken['sType'] == "WORD" and not oSpellChecker.isValidToken(dToken['sValue']):
            if bSpellSugg:
                dToken['aSuggestions'] = []
                for lSugg in oSpellChecker.suggest(dToken['sValue']):
                    dToken['aSuggestions'].extend(lSugg)
            aSpellErrs.append(dToken)
    return aGrammErrs, aSpellErrs


def generateText (sText, oTokenizer, oSpellChecker, bDebug=False, bEmptyIfNoErrors=False, bSpellSugg=False, nWidth=100):
    aGrammErrs, aSpellErrs = _getErrors(sText, oTokenizer, oSpellChecker, False, bSpellSugg, bDebug)
    if bEmptyIfNoErrors and not aGrammErrs and not aSpellErrs:
        return ""
    return txt.generateParagraph(sText, aGrammErrs, aSpellErrs, nWidth)


def generateJSON (iIndex, sText, oTokenizer, oSpellChecker, bContext=False, bDebug=False, bEmptyIfNoErrors=False, bSpellSugg=False, lLineSet=None, bReturnText=False):
    aGrammErrs, aSpellErrs = _getErrors(sText, oTokenizer, oSpellChecker, bContext, bSpellSugg, bDebug)
    aGrammErrs = list(aGrammErrs)
    if bEmptyIfNoErrors and not aGrammErrs and not aSpellErrs:
        return ""
    if lLineSet:
        aGrammErrs, aSpellErrs = txt.convertToXY(aGrammErrs, aSpellErrs, lLineSet)
        return json.dumps({ "lGrammarErrors": aGrammErrs, "lSpellingErrors": aSpellErrs }, ensure_ascii=False)
    if bReturnText:
        return json.dumps({ "iParagraph": iIndex, "sText": sText, "lGrammarErrors": aGrammErrs, "lSpellingErrors": aSpellErrs }, ensure_ascii=False)
    return json.dumps({ "iParagraph": iIndex, "lGrammarErrors": aGrammErrs, "lSpellingErrors": aSpellErrs }, ensure_ascii=False)


def readfile (spf):
    "generator: returns file line by line"
    if os.path.isfile(spf):
        with open(spf, "r", encoding="utf-8") as hSrc:
            for sLine in hSrc:
                yield sLine
    else:
        print("# Error: file not found.")


def readfileAndConcatLines (spf):
    "generator: returns text by list of lines not separated by an empty line"




    lLine = []

    for i, sLine in enumerate(readfile(spf), 1):
        if sLine.strip():
            lLine.append((i, sLine))
        elif lLine:

            yield lLine
            lLine = []

    if lLine:

        yield lLine


def output (sText, hDst=None):
    if not hDst:
        echo(sText, end="")
    else:
        hDst.write(sText)


def main ():
    xParser = argparse.ArgumentParser()
    xParser.add_argument("-f", "--file", help="parse file (UTF-8 required!) [on Windows, -f is similar to -ff]", type=str)
    xParser.add_argument("-ff", "--file_to_file", help="parse file (UTF-8 required!) and create a result file (*.res.txt)", type=str)
    xParser.add_argument("-owe", "--only_when_errors", help="display results only when there are errors", action="store_true")
    xParser.add_argument("-j", "--json", help="generate list of errors in JSON (only with option --file or --file_to_file)", action="store_true")
    xParser.add_argument("-cl", "--concat_lines", help="concatenate lines not separated by an empty paragraph (only with option --file or --file_to_file)", action="store_true")
    xParser.add_argument("-tf", "--textformatter", help="auto-format text according to typographical rules (unavailable with option --concat_lines)", action="store_true")
    xParser.add_argument("-tfo", "--textformatteronly", help="auto-format text and disable grammar checking (only with option --file or --file_to_file)", action="store_true")
    xParser.add_argument("-ctx", "--context", help="return errors with context (only with option --json)", action="store_true")
    xParser.add_argument("-wss", "--with_spell_sugg", help="add suggestions for spelling errors (only with option --file or --file_to_file)", action="store_true")
    xParser.add_argument("-w", "--width", help="width in characters (40 < width < 200; default: 100)", type=int, choices=range(40,201,10), default=100)
    xParser.add_argument("-lo", "--list_options", help="list options", action="store_true")
    xParser.add_argument("-lr", "--list_rules", nargs="?", help="list rules [regex pattern as filter]", const="*")
    xParser.add_argument("-sug", "--suggest", help="get suggestions list for given word", type=str)
    xParser.add_argument("-on", "--opt_on", nargs="+", help="activate options")
    xParser.add_argument("-off", "--opt_off", nargs="+", help="deactivate options")
    xParser.add_argument("-roff", "--rule_off", nargs="+", help="deactivate rules")
    xParser.add_argument("-d", "--debug", help="debugging mode (only in interactive mode)", action="store_true")
    xArgs = xParser.parse_args()





    gce.load()
    if not xArgs.json:
        echo("Grammalecte v{}".format(gce.version))
    oSpellChecker = gce.getSpellChecker()
    oTokenizer = tkz.Tokenizer("fr")
    oLexGraphe = lxg.Lexicographe(oSpellChecker)
    if xArgs.textformatter or xArgs.textformatteronly:
        oTF = tf.TextFormatter()


    if xArgs.list_options or xArgs.list_rules:
        if xArgs.list_options:
            gce.displayOptions("fr")
        if xArgs.list_rules:
            gce.displayRules(None  if xArgs.list_rules == "*"  else xArgs.list_rules)
        exit()


    if xArgs.suggest:
        for lSugg in oSpellChecker.suggest(xArgs.suggest):
            if xArgs.json:
                sText = json.dumps({ "aSuggestions": lSugg }, ensure_ascii=False)
            else:
                sText = "Suggestions : " + " | ".join(lSugg)
            echo(sText)
        exit()


    if not xArgs.json:
        xArgs.context = False




    gce.setOptions({"html": True, "latex": True})
    if xArgs.opt_on:
        gce.setOptions({ opt:True  for opt in xArgs.opt_on  if opt in gce.getOptions() })
    if xArgs.opt_off:
        gce.setOptions({ opt:False  for opt in xArgs.opt_off  if opt in gce.getOptions() })


    if xArgs.rule_off:
        for sRule in xArgs.rule_off:
            gce.ignoreRule(sRule)

    sFile = xArgs.file or xArgs.file_to_file
    if sFile:
        # file processing
        hDst = open(sFile[:sFile.rfind(".")]+".res.txt", "w", encoding="utf-8", newline="\n")  if xArgs.file_to_file or sys.platform == "win32"  else None
        bComma = False
        if xArgs.json:
            output('{ "grammalecte": "'+gce.version+'", "lang": "'+gce.lang+'", "data" : [\n', hDst)
        if not xArgs.concat_lines:
            # pas de concaténation des lignes
            for i, sText in enumerate(readfile(sFile), 1):
                if xArgs.textformatter or xArgs.textformatteronly:
                    sText = oTF.formatText(sText)
                if xArgs.textformatteronly:
                    output(sText, hDst)
                else:

                    if xArgs.json:
                        sText = generateJSON(i, sText, oTokenizer, oSpellChecker, bContext=xArgs.context, bDebug=False, bEmptyIfNoErrors=xArgs.only_when_errors, bSpellSugg=xArgs.with_spell_sugg, bReturnText=xArgs.textformatter)
                    else:
                        sText = generateText(sText, oTokenizer, oSpellChecker, bDebug=False, bEmptyIfNoErrors=xArgs.only_when_errors, bSpellSugg=xArgs.with_spell_sugg, nWidth=xArgs.width)
                    if sText:
                        if xArgs.json and bComma:
                            output(",\n", hDst)
                        output(sText, hDst)
                        bComma = True
                if hDst:
                    echo("§ %d\r" % i, end="", flush=True)
        else:
            # concaténation des lignes non séparées par une ligne vide
            for i, lLine in enumerate(readfileAndConcatLines(sFile), 1):
                sText, lLineSet = txt.createParagraphWithLines(lLine)
                if xArgs.json:
                    sText = generateJSON(i, sText, oTokenizer, oSpellChecker, bContext=xArgs.context, bDebug=False, bEmptyIfNoErrors=xArgs.only_when_errors, bSpellSugg=xArgs.with_spell_sugg, lLineSet=lLineSet)
                else:
                    sText = generateText(sText, oTokenizer, oSpellChecker, bDebug=False, bEmptyIfNoErrors=xArgs.only_when_errors, bSpellSugg=xArgs.with_spell_sugg, nWidth=xArgs.width)
                if sText:
                    if xArgs.json and bComma:
                        output(",\n", hDst)
                    output(sText, hDst)
                    bComma = True
                if hDst:
                    echo("§ %d\r" % i, end="", flush=True)
        if xArgs.json:
            output("\n]}\n", hDst)
    else:
        # pseudo-console
        sInputText = "\n~==========~ Enter your text [/h /q] ~==========~\n"
        sText = _getText(sInputText)
        while True:
            if sText.startswith("?"):
                for sWord in sText[1:].strip().split():
                    if sWord:
                        echo("* " + sWord)
                        for sMorph in oSpellChecker.getMorph(sWord):
                            echo("  {:<32} {}".format(sMorph, oLexGraphe.formatTags(sMorph)))
            elif sText.startswith("!"):
                for sWord in sText[1:].strip().split():
                    if sWord:
                        for lSugg in oSpellChecker.suggest(sWord):
                            echo(" | ".join(lSugg))
            elif sText.startswith(">"):
                oSpellChecker.drawPath(sText[1:].strip())
            elif sText.startswith("="):
                for sRes in oSpellChecker.select(sText[1:].strip()):
                    echo(sRes)
            elif sText.startswith("/+ "):
                gce.setOptions({ opt:True  for opt in sText[3:].strip().split()  if opt in gce.getOptions() })
                echo("done")
            elif sText.startswith("/- "):
                gce.setOptions({ opt:False  for opt in sText[3:].strip().split()  if opt in gce.getOptions() })
                echo("done")
            elif sText.startswith("/-- "):
                for sRule in sText[3:].strip().split():
                    gce.ignoreRule(sRule)
                echo("done")
            elif sText.startswith("/++ "):
                for sRule in sText[3:].strip().split():
                    gce.reactivateRule(sRule)
                echo("done")
            elif sText == "/debug" or sText == "/d":
                xArgs.debug = not(xArgs.debug)
                echo("debug mode on"  if xArgs.debug  else "debug mode off")
            elif sText == "/textformatter" or sText == "/tf":
                xArgs.textformatter = not(xArgs.textformatter)
                echo("textformatter on"  if xArgs.debug  else "textformatter off")
            elif sText == "/help" or sText == "/h":
                echo(_HELP)
            elif sText == "/lopt" or sText == "/lo":
                gce.displayOptions("fr")
            elif sText.startswith("/lr"):
                sText = sText.strip()
                sFilter = sText[sText.find(" "):].strip()  if sText != "/lr" and sText != "/rules"  else None
                gce.displayRules(sFilter)
            elif sText == "/quit" or sText == "/q":
                break
            elif sText.startswith("/rl"):
                # reload (todo)
                pass
            else:
                for sParagraph in txt.getParagraph(sText):
                    if xArgs.textformatter:
                        sText = oTF.formatText(sText)
                    sRes = generateText(sText, oTokenizer, oSpellChecker, bDebug=xArgs.debug, bEmptyIfNoErrors=xArgs.only_when_errors, nWidth=xArgs.width)
                    if sRes:
                        echo("\n" + sRes)
                    else:
                        echo("\nNo error found.")
            sText = _getText(sInputText)


if __name__ == '__main__':
    main()







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|






|


|
|
>
>
>
>
|
>
|
|
|
|
>
|
|
>
|
>
|
















|













>
>
>
>
|

|
<
<
<
<
<

>


|

|


>









>


>
>

>
|

|

|

>


|







|
|
<
<
|
|
|
|
<
>
|
|
<
|
<
<
<
<
<
<
<
|
<
<
<
<
<
<
|
|
|
|
|
|
|
|












|











|


|



|



|










|



|








|
|









37
38
39
40
41
42
43


































44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109





110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154


155
156
157
158

159
160
161

162







163






164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
    if sys.platform == "win32":
        # Apparently, the console transforms «’» in «'».
        # So we reverse it to avoid many useless warnings.
        sText = sText.replace("'", "’")
    return sText




































def readFile (spf):
    "generator: returns file line by line"
    if os.path.isfile(spf):
        with open(spf, "r", encoding="utf-8") as hSrc:
            for sLine in hSrc:
                yield sLine
    else:
        print("# Error: file <" + spf + ">not found.")


def generateParagraphFromFile (spf, bConcatLines=False):
    "generator: returns text by tuple of (iParagraph, sParagraph, lLineSet)"
    if not bConcatLines:
        for iParagraph, sLine in enumerate(readFile(spf), 1):
            yield iParagraph, sLine, None
    else:
        lLine = []
        iParagraph = 1
        for iLine, sLine in enumerate(readFile(spf), 1):
            if sLine.strip():
                lLine.append((iLine, sLine))
            elif lLine:
                sText, lLineSet = txt.createParagraphWithLines(lLine)
                yield iParagraph, sText, lLineSet
                lLine = []
            iParagraph += 1
        if lLine:
            sText, lLineSet = txt.createParagraphWithLines(lLine)
            yield iParagraph, sText, lLineSet


def output (sText, hDst=None):
    if not hDst:
        echo(sText, end="")
    else:
        hDst.write(sText)


def main ():
    xParser = argparse.ArgumentParser()
    xParser.add_argument("-f", "--file", help="parse file (UTF-8 required!) [on Windows, -f is similar to -ff]", type=str)
    xParser.add_argument("-ff", "--file_to_file", help="parse file (UTF-8 required!) and create a result file (*.res.txt)", type=str)
    xParser.add_argument("-owe", "--only_when_errors", help="display results only when there are errors", action="store_true")
    xParser.add_argument("-j", "--json", help="generate list of errors in JSON (only with option --file or --file_to_file)", action="store_true")
    xParser.add_argument("-cl", "--concat_lines", help="concatenate lines not separated by an empty paragraph (only with option --file or --file_to_file)", action="store_true")
    xParser.add_argument("-tf", "--textformatter", help="auto-format text according to typographical rules (not with option --concat_lines)", action="store_true")
    xParser.add_argument("-tfo", "--textformatteronly", help="auto-format text and disable grammar checking (only with option --file or --file_to_file)", action="store_true")
    xParser.add_argument("-ctx", "--context", help="return errors with context (only with option --json)", action="store_true")
    xParser.add_argument("-wss", "--with_spell_sugg", help="add suggestions for spelling errors (only with option --file or --file_to_file)", action="store_true")
    xParser.add_argument("-w", "--width", help="width in characters (40 < width < 200; default: 100)", type=int, choices=range(40,201,10), default=100)
    xParser.add_argument("-lo", "--list_options", help="list options", action="store_true")
    xParser.add_argument("-lr", "--list_rules", nargs="?", help="list rules [regex pattern as filter]", const="*")
    xParser.add_argument("-sug", "--suggest", help="get suggestions list for given word", type=str)
    xParser.add_argument("-on", "--opt_on", nargs="+", help="activate options")
    xParser.add_argument("-off", "--opt_off", nargs="+", help="deactivate options")
    xParser.add_argument("-roff", "--rule_off", nargs="+", help="deactivate rules")
    xParser.add_argument("-d", "--debug", help="debugging mode (only in interactive mode)", action="store_true")
    xArgs = xParser.parse_args()

    oGrammarChecker = grammalecte.GrammarChecker("fr")
    oSpellChecker = oGrammarChecker.getSpellChecker()
    oLexicographer = oGrammarChecker.getLexicographer()
    oTextFormatter = oGrammarChecker.getTextFormatter()

    if not xArgs.json:
        echo("Grammalecte v{}".format(oGrammarChecker.gce.version))






    # list options or rules
    if xArgs.list_options or xArgs.list_rules:
        if xArgs.list_options:
            oGrammarChecker.gce.displayOptions("fr")
        if xArgs.list_rules:
            oGrammarChecker.gce.displayRules(None  if xArgs.list_rules == "*"  else xArgs.list_rules)
        exit()

    # spell suggestions
    if xArgs.suggest:
        for lSugg in oSpellChecker.suggest(xArgs.suggest):
            if xArgs.json:
                sText = json.dumps({ "aSuggestions": lSugg }, ensure_ascii=False)
            else:
                sText = "Suggestions : " + " | ".join(lSugg)
            echo(sText)
        exit()

    # disable options
    if not xArgs.json:
        xArgs.context = False
    if xArgs.concat_lines:
        xArgs.textformatter = False

    # grammar options
    oGrammarChecker.gce.setOptions({"html": True, "latex": True})
    if xArgs.opt_on:
        oGrammarChecker.gce.setOptions({ opt:True  for opt in xArgs.opt_on  if opt in oGrammarChecker.gce.getOptions() })
    if xArgs.opt_off:
        oGrammarChecker.gce.setOptions({ opt:False  for opt in xArgs.opt_off  if opt in oGrammarChecker.gce.getOptions() })

    # disable grammar rules
    if xArgs.rule_off:
        for sRule in xArgs.rule_off:
            oGrammarChecker.gce.ignoreRule(sRule)

    sFile = xArgs.file or xArgs.file_to_file
    if sFile:
        # file processing
        hDst = open(sFile[:sFile.rfind(".")]+".res.txt", "w", encoding="utf-8", newline="\n")  if xArgs.file_to_file or sys.platform == "win32"  else None
        bComma = False
        if xArgs.json:
            output('{ "grammalecte": "'+oGrammarChecker.gce.version+'", "lang": "'+oGrammarChecker.gce.lang+'", "data" : [\n', hDst)
        for i, sText, lLineSet in generateParagraphFromFile(sFile, xArgs.concat_lines):


            if xArgs.textformatter or xArgs.textformatteronly:
                sText = oTextFormatter.formatText(sText)
            if xArgs.textformatteronly:
                output(sText, hDst)

                continue
            if xArgs.json:
                sText = oGrammarChecker.generateParagraphAsJSON(i, sText, bContext=xArgs.context, bEmptyIfNoErrors=xArgs.only_when_errors, \

                                                                bSpellSugg=xArgs.with_spell_sugg, bReturnText=xArgs.textformatter, lLineSet=lLineSet)







            else:






                sText = oGrammarChecker.generateParagraph(sText, bEmptyIfNoErrors=xArgs.only_when_errors, bSpellSugg=xArgs.with_spell_sugg, nWidth=xArgs.width)
            if sText:
                if xArgs.json and bComma:
                    output(",\n", hDst)
                output(sText, hDst)
                bComma = True
            if hDst:
                echo("§ %d\r" % i, end="", flush=True)
        if xArgs.json:
            output("\n]}\n", hDst)
    else:
        # pseudo-console
        sInputText = "\n~==========~ Enter your text [/h /q] ~==========~\n"
        sText = _getText(sInputText)
        while True:
            if sText.startswith("?"):
                for sWord in sText[1:].strip().split():
                    if sWord:
                        echo("* " + sWord)
                        for sMorph in oSpellChecker.getMorph(sWord):
                            echo("  {:<32} {}".format(sMorph, oLexicographer.formatTags(sMorph)))
            elif sText.startswith("!"):
                for sWord in sText[1:].strip().split():
                    if sWord:
                        for lSugg in oSpellChecker.suggest(sWord):
                            echo(" | ".join(lSugg))
            elif sText.startswith(">"):
                oSpellChecker.drawPath(sText[1:].strip())
            elif sText.startswith("="):
                for sRes in oSpellChecker.select(sText[1:].strip()):
                    echo(sRes)
            elif sText.startswith("/+ "):
                oGrammarChecker.gce.setOptions({ opt:True  for opt in sText[3:].strip().split()  if opt in oGrammarChecker.gce.getOptions() })
                echo("done")
            elif sText.startswith("/- "):
                oGrammarChecker.gce.setOptions({ opt:False  for opt in sText[3:].strip().split()  if opt in oGrammarChecker.gce.getOptions() })
                echo("done")
            elif sText.startswith("/-- "):
                for sRule in sText[3:].strip().split():
                    oGrammarChecker.gce.ignoreRule(sRule)
                echo("done")
            elif sText.startswith("/++ "):
                for sRule in sText[3:].strip().split():
                    oGrammarChecker.gce.reactivateRule(sRule)
                echo("done")
            elif sText == "/debug" or sText == "/d":
                xArgs.debug = not(xArgs.debug)
                echo("debug mode on"  if xArgs.debug  else "debug mode off")
            elif sText == "/textformatter" or sText == "/tf":
                xArgs.textformatter = not(xArgs.textformatter)
                echo("textformatter on"  if xArgs.debug  else "textformatter off")
            elif sText == "/help" or sText == "/h":
                echo(_HELP)
            elif sText == "/lopt" or sText == "/lo":
                oGrammarChecker.gce.displayOptions("fr")
            elif sText.startswith("/lr"):
                sText = sText.strip()
                sFilter = sText[sText.find(" "):].strip()  if sText != "/lr" and sText != "/rules"  else None
                oGrammarChecker.gce.displayRules(sFilter)
            elif sText == "/quit" or sText == "/q":
                break
            elif sText.startswith("/rl"):
                # reload (todo)
                pass
            else:
                for sParagraph in txt.getParagraph(sText):
                    if xArgs.textformatter:
                        sText = oTextFormatter.formatText(sText)
                    sRes = oGrammarChecker.generateParagraph(sText, bEmptyIfNoErrors=xArgs.only_when_errors, nWidth=xArgs.width, bDebug=xArgs.debug)
                    if sRes:
                        echo("\n" + sRes)
                    else:
                        echo("\nNo error found.")
            sText = _getText(sInputText)


if __name__ == '__main__':
    main()

Modified grammalecte-server.py from [3253a2b2ff] to [a5cc9d7be7].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 #!/usr/bin/env python3

import sys
import os.path
import argparse
import json
import traceback
import configparser
import time

from bottle import Bottle, run, request, response, template, static_file

import grammalecte.fr as gce
import grammalecte.fr.lexicographe as lxg
import grammalecte.fr.textformatter as tf
import grammalecte.text as txt
import grammalecte.graphspell.tokenizer as tkz
from grammalecte.graphspell.echo import echo


HOMEPAGE = """
<!DOCTYPE HTML>
<html>
    <head>












<
|
<

<







1
2
3
4
5
6
7
8
9
10
11
12

13

14

15
16
17
18
19
20
21
 #!/usr/bin/env python3

import sys
import os.path
import argparse
import json
import traceback
import configparser
import time

from bottle import Bottle, run, request, response, template, static_file


import grammalecte

import grammalecte.text as txt

from grammalecte.graphspell.echo import echo


HOMEPAGE = """
<!DOCTYPE HTML>
<html>
    <head>
125
126
127
128
129
130
131

132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def genUserId ():
    i = 0
    while True:
        yield str(i)
        i += 1



def parseParagraph (iParagraph, sText, oTokenizer, oSpellChecker, dOptions, bDebug=False, bEmptyIfNoErrors=False):
    aGrammErrs = gce.parse(sText, "FR", bDebug, dOptions)
    aGrammErrs = list(aGrammErrs)
    aSpellErrs = []
    for dToken in oTokenizer.genTokens(sText):
        if dToken['sType'] == "WORD" and not oSpellChecker.isValidToken(dToken['sValue']):
            aSpellErrs.append(dToken)
    if bEmptyIfNoErrors and not aGrammErrs and not aSpellErrs:
        return ""
    return "  " + json.dumps({ "iParagraph": iParagraph, "lGrammarErrors": aGrammErrs, "lSpellingErrors": aSpellErrs }, ensure_ascii=False)
    

if __name__ == '__main__':

    gce.load("Server")
    echo("Grammalecte v{}".format(gce.version))
    dServerOptions = getServerOptions()
    dGCOptions = getConfigOptions("fr")
    if dGCOptions:
        gce.setOptions(dGCOptions)
    dServerGCOptions = gce.getOptions()
    echo("Grammar options:\n" + " | ".join([ k + ": " + str(v)  for k, v in sorted(dServerGCOptions.items()) ]))
    oSpellChecker = gce.getSpellChecker()
    oTokenizer = tkz.Tokenizer("fr")
    oTF = tf.TextFormatter()
    dUser = {}
    userGenerator = genUserId()

    app = Bottle()

    # GET
    @app.route("/")







>
|
|
|
|
<
|
<
<
<
<
|
|
<

<







<
<
<







122
123
124
125
126
127
128
129
130
131
132
133

134




135
136

137

138
139
140
141
142
143
144



145
146
147
148
149
150
151
def genUserId ():
    i = 0
    while True:
        yield str(i)
        i += 1


if __name__ == '__main__':

    # initialisation
    oGrammarChecker = grammalecte.GrammarChecker("fr", "Server")
    oSpellChecker = oGrammarChecker.getSpellChecker()

    oLexicographer = oGrammarChecker.getLexicographer()




    oTextFormatter = oGrammarChecker.getTextFormatter()
    gce = oGrammarChecker.getGCEngine()



    echo("Grammalecte v{}".format(gce.version))
    dServerOptions = getServerOptions()
    dGCOptions = getConfigOptions("fr")
    if dGCOptions:
        gce.setOptions(dGCOptions)
    dServerGCOptions = gce.getOptions()
    echo("Grammar options:\n" + " | ".join([ k + ": " + str(v)  for k, v in sorted(dServerGCOptions.items()) ]))



    dUser = {}
    userGenerator = genUserId()

    app = Bottle()

    # GET
    @app.route("/")
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209

    # POST
    @app.route("/gc_text/fr", method="POST")
    def gcText ():
        #if len(lang) != 2 or lang != "fr":
        #    abort(404, "No grammar checker available for lang “" + str(lang) + "”")
        bComma = False
        bTF = bool(request.forms.tf)
        dOptions = None
        sError = ""
        if request.cookies.user_id:
            if request.cookies.user_id in dUser:
                dOptions = dUser[request.cookies.user_id].get("gc_options", None)
                response.set_cookie("user_id", request.cookies.user_id, path="/", max_age=86400) # we renew cookie for 24h
            else:
                response.delete_cookie("user_id", path="/")
        if request.forms.options:
            try:
                dOptions = dict(dServerGCOptions)  if not dOptions  else dict(dOptions)
                dOptions.update(json.loads(request.forms.options))
            except:
                sError = "request options not used"
        sJSON = '{ "program": "grammalecte-fr", "version": "'+gce.version+'", "lang": "'+gce.lang+'", "error": "'+sError+'", "data" : [\n'
        for i, sText in enumerate(txt.getParagraph(request.forms.text), 1):
            if bTF:
                sText = oTF.formatText(sText)
            sText = parseParagraph(i, sText, oTokenizer, oSpellChecker, dOptions, bEmptyIfNoErrors=True)
            if sText:
                if bComma:
                    sJSON += ",\n"
                sJSON += sText
                bComma = True
        sJSON += "\n]}\n"
        return sJSON







<
















|
|
|







164
165
166
167
168
169
170

171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196

    # POST
    @app.route("/gc_text/fr", method="POST")
    def gcText ():
        #if len(lang) != 2 or lang != "fr":
        #    abort(404, "No grammar checker available for lang “" + str(lang) + "”")
        bComma = False

        dOptions = None
        sError = ""
        if request.cookies.user_id:
            if request.cookies.user_id in dUser:
                dOptions = dUser[request.cookies.user_id].get("gc_options", None)
                response.set_cookie("user_id", request.cookies.user_id, path="/", max_age=86400) # we renew cookie for 24h
            else:
                response.delete_cookie("user_id", path="/")
        if request.forms.options:
            try:
                dOptions = dict(dServerGCOptions)  if not dOptions  else dict(dOptions)
                dOptions.update(json.loads(request.forms.options))
            except:
                sError = "request options not used"
        sJSON = '{ "program": "grammalecte-fr", "version": "'+gce.version+'", "lang": "'+gce.lang+'", "error": "'+sError+'", "data" : [\n'
        for i, sText in enumerate(txt.getParagraph(request.forms.text), 1):
            if bool(request.forms.tf):
                sText = oTextFormatter.formatText(sText)
            sText = oGrammarChecker.generateParagraphAsJSON(i, sText, dOptions=dOptions, bEmptyIfNoErrors=True, bReturnText=bool(request.forms.tf))
            if sText:
                if bComma:
                    sJSON += ",\n"
                sJSON += sText
                bComma = True
        sJSON += "\n]}\n"
        return sJSON
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
    def resetOptions ():
        if request.cookies.user_id and request.cookies.user_id in dUser:
            del dUser[request.cookies.user_id]
        return "done"

    @app.route("/format_text/fr", method="POST")
    def formatText ():
        return oTF.formatText(request.forms.text)

    #@app.route('/static/<filepath:path>')
    #def server_static (filepath):
    #    return static_file(filepath, root='./views/static')

    @app.route("/purge_users", method="POST")
    def purgeUsers ():







|







214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
    def resetOptions ():
        if request.cookies.user_id and request.cookies.user_id in dUser:
            del dUser[request.cookies.user_id]
        return "done"

    @app.route("/format_text/fr", method="POST")
    def formatText ():
        return oTextFormatter.formatText(request.forms.text)

    #@app.route('/static/<filepath:path>')
    #def server_static (filepath):
    #    return static_file(filepath, root='./views/static')

    @app.route("/purge_users", method="POST")
    def purgeUsers ():

Modified graphspell-js/spellchecker.js from [e878cd2181] to [7b8a526c88].

9
10
11
12
13
14
15

16
17
18
19
20
21
22


"use strict";


if (typeof(require) !== 'undefined') {
    var ibdawg = require("resource://grammalecte/graphspell/ibdawg.js");

}


${map}


const dDefaultDictionaries = new Map([







>







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23


"use strict";


if (typeof(require) !== 'undefined') {
    var ibdawg = require("resource://grammalecte/graphspell/ibdawg.js");
    var tokenizer = require("resource://grammalecte/graphspell/tokenizer.js");
}


${map}


const dDefaultDictionaries = new Map([
32
33
34
35
36
37
38

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62








63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80















81
82
83
84
85
86
87
        this.sLangCode = sLangCode;
        if (!mainDic) {
            mainDic = dDefaultDictionaries.gl_get(sLangCode, "");
        }
        this.oMainDic = this._loadDictionary(mainDic, sPath, true);
        this.oExtendedDic = this._loadDictionary(extentedDic, sPath);
        this.oPersonalDic = this._loadDictionary(personalDic, sPath);

    }

    _loadDictionary (dictionary, sPath, bNecessary=false) {
        // returns an IBDAWG object
        if (!dictionary) {
            return null;
        }
        try {
            if (typeof(require) !== 'undefined') {
                return new ibdawg.IBDAWG(dictionary);  // dictionary can be a filename or a JSON object
            } else {
                return new IBDAWG(dictionary, sPath);  // dictionary can be a filename or a JSON object
            }
        }
        catch (e) {
            let sfDictionary = (typeof(dictionary) == "string") ? dictionary : dictionary.sLangName + "/" + dictionary.sFileName;
            if (bNecessary) {
                throw "Error: <" + sfDictionary + "> not loaded. " + e.message;
            }
            console.log("Error: <" + sfDictionary + "> not loaded.")
            console.log(e.message);
            return null;
        }
    }









    setMainDictionary (dictionary) {
        // returns true if the dictionary is loaded
        this.oMainDic = this._loadDictionary(dictionary);
        return Boolean(this.oMainDic);
    }

    setExtendedDictionary (dictionary) {
        // returns true if the dictionary is loaded
        this.oExtendedDic = this._loadDictionary(dictionary);
        return Boolean(this.oExtendedDic);
    }

    setPersonalDictionary (dictionary) {
        // returns true if the dictionary is loaded
        this.oPersonalDic = this._loadDictionary(dictionary);
        return Boolean(this.oPersonalDic);
    }
















    // IBDAWG functions

    isValidToken (sToken) {
        // checks if sToken is valid (if there is hyphens in sToken, sToken is split, each part is checked)
        if (this.oMainDic.isValidToken(sToken)) {
            return true;







>








|















>
>
>
>
>
>
>
>


















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
        this.sLangCode = sLangCode;
        if (!mainDic) {
            mainDic = dDefaultDictionaries.gl_get(sLangCode, "");
        }
        this.oMainDic = this._loadDictionary(mainDic, sPath, true);
        this.oExtendedDic = this._loadDictionary(extentedDic, sPath);
        this.oPersonalDic = this._loadDictionary(personalDic, sPath);
        this.oTokenizer = null;
    }

    _loadDictionary (dictionary, sPath, bNecessary=false) {
        // returns an IBDAWG object
        if (!dictionary) {
            return null;
        }
        try {
            if (typeof(ibdawg) !== 'undefined') {
                return new ibdawg.IBDAWG(dictionary);  // dictionary can be a filename or a JSON object
            } else {
                return new IBDAWG(dictionary, sPath);  // dictionary can be a filename or a JSON object
            }
        }
        catch (e) {
            let sfDictionary = (typeof(dictionary) == "string") ? dictionary : dictionary.sLangName + "/" + dictionary.sFileName;
            if (bNecessary) {
                throw "Error: <" + sfDictionary + "> not loaded. " + e.message;
            }
            console.log("Error: <" + sfDictionary + "> not loaded.")
            console.log(e.message);
            return null;
        }
    }

    loadTokenizer () {
        if (typeof(tokenizer) !== 'undefined') {
            this.oTokenizer = new tokenizer.Tokenizer(this.sLangCode);
        } else {
            this.oTokenizer = new Tokenizer(this.sLangCode);
        }
    }

    setMainDictionary (dictionary) {
        // returns true if the dictionary is loaded
        this.oMainDic = this._loadDictionary(dictionary);
        return Boolean(this.oMainDic);
    }

    setExtendedDictionary (dictionary) {
        // returns true if the dictionary is loaded
        this.oExtendedDic = this._loadDictionary(dictionary);
        return Boolean(this.oExtendedDic);
    }

    setPersonalDictionary (dictionary) {
        // returns true if the dictionary is loaded
        this.oPersonalDic = this._loadDictionary(dictionary);
        return Boolean(this.oPersonalDic);
    }

    // parse text functions

    parseParagraph (sText) {
        if (!this.oTokenizer) {
            this.loadTokenizer();
        }
        let aSpellErr = [];
        for (let oToken of this.oTokenizer.genTokens(sText)) {
            if (oToken.sType === 'WORD' && !this.isValidToken(oToken.sValue)) {
                aSpellErr.push(oToken);
            }
        }
        return aSpellErr;
    }

    // IBDAWG functions

    isValidToken (sToken) {
        // checks if sToken is valid (if there is hyphens in sToken, sToken is split, each part is checked)
        if (this.oMainDic.isValidToken(sToken)) {
            return true;

Modified graphspell-js/tokenizer.js from [c3f0ee8c90] to [bdd895b918].

83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
                    helpers.logerror(e);
                }
            }
            i += nCut;
            sText = sText.slice(nCut);
        }
    }

    getSpellingErrors (sText, oSpellChecker) {
        let aSpellErr = [];
        for (let oToken of this.genTokens(sText)) {
            if (oToken.sType === 'WORD' && !oSpellChecker.isValidToken(oToken.sValue)) {
                aSpellErr.push(oToken);
            }
        }
        return aSpellErr;
    }
}


if (typeof(exports) !== 'undefined') {
    exports.Tokenizer = Tokenizer;
}







<
<
<
<
<
<
<
<
<
<






83
84
85
86
87
88
89










90
91
92
93
94
95
                    helpers.logerror(e);
                }
            }
            i += nCut;
            sText = sText.slice(nCut);
        }
    }










}


if (typeof(exports) !== 'undefined') {
    exports.Tokenizer = Tokenizer;
}

Modified graphspell/__init__.py from [a7ffc6f8bf] to [a53bdfb757].





>
>
1
2

from .spellchecker import *

Modified graphspell/spellchecker.py from [638f8d8cdf] to [dbd02131cc].

1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

32
33
34
35
36
37
38
39
40
41
42
43
44
45



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60






























61
62
63
64
65
66
67
# Spellchecker
# Wrapper for the IBDAWG class.
# Useful to check several dictionaries at once.

# To avoid iterating over a pile of dictionaries, it is assumed that 3 are enough:
# - the main dictionary, bundled with the package
# - the extended dictionary, added by an organization
# - the personal dictionary, created by the user for its own convenience


import traceback

from . import ibdawg



dDefaultDictionaries = {
    "fr": "fr.bdic",
    "en": "en.bdic"
}


class SpellChecker ():

    def __init__ (self, sLangCode, sfMainDic="", sfExtendedDic="", sfPersonalDic=""):
        "returns True if the main dictionary is loaded"
        self.sLangCode = sLangCode
        if not sfMainDic:
            sfMainDic = dDefaultDictionaries.get(sLangCode, "")
        self.oMainDic = self._loadDictionary(sfMainDic, True)
        self.oExtendedDic = self._loadDictionary(sfExtendedDic)
        self.oPersonalDic = self._loadDictionary(sfPersonalDic)


    def _loadDictionary (self, sfDictionary, bNecessary=False):
        "returns an IBDAWG object"
        if not sfDictionary:
            return None
        try:
            return ibdawg.IBDAWG(sfDictionary)
        except Exception as e:
            if bNecessary:
                raise Exception(str(e), "Error: <" + sfDictionary + "> not loaded.")
            print("Error: <" + sfDictionary + "> not loaded.")
            traceback.print_exc()
            return None




    def setMainDictionary (self, sfDictionary):
        "returns True if the dictionary is loaded"
        self.oMainDic = self._loadDictionary(sfDictionary)
        return bool(self.oMainDic)
            
    def setExtendedDictionary (self, sfDictionary):
        "returns True if the dictionary is loaded"
        self.oExtendedDic = self._loadDictionary(sfDictionary)
        return bool(self.oExtendedDic)

    def setPersonalDictionary (self, sfDictionary):
        "returns True if the dictionary is loaded"
        self.oPersonalDic = self._loadDictionary(sfDictionary)
        return bool(self.oPersonalDic)
































    # IBDAWG functions

    def isValidToken (self, sToken):
        "checks if sToken is valid (if there is hyphens in sToken, sToken is split, each part is checked)"
        if self.oMainDic.isValidToken(sToken):
            return True













>


















>














>
>
>















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# Spellchecker
# Wrapper for the IBDAWG class.
# Useful to check several dictionaries at once.

# To avoid iterating over a pile of dictionaries, it is assumed that 3 are enough:
# - the main dictionary, bundled with the package
# - the extended dictionary, added by an organization
# - the personal dictionary, created by the user for its own convenience


import traceback

from . import ibdawg
from . import tokenizer


dDefaultDictionaries = {
    "fr": "fr.bdic",
    "en": "en.bdic"
}


class SpellChecker ():

    def __init__ (self, sLangCode, sfMainDic="", sfExtendedDic="", sfPersonalDic=""):
        "returns True if the main dictionary is loaded"
        self.sLangCode = sLangCode
        if not sfMainDic:
            sfMainDic = dDefaultDictionaries.get(sLangCode, "")
        self.oMainDic = self._loadDictionary(sfMainDic, True)
        self.oExtendedDic = self._loadDictionary(sfExtendedDic)
        self.oPersonalDic = self._loadDictionary(sfPersonalDic)
        self.oTokenizer = None

    def _loadDictionary (self, sfDictionary, bNecessary=False):
        "returns an IBDAWG object"
        if not sfDictionary:
            return None
        try:
            return ibdawg.IBDAWG(sfDictionary)
        except Exception as e:
            if bNecessary:
                raise Exception(str(e), "Error: <" + sfDictionary + "> not loaded.")
            print("Error: <" + sfDictionary + "> not loaded.")
            traceback.print_exc()
            return None

    def loadTokenizer (self):
        self.oTokenizer = tokenizer.Tokenizer(self.sLangCode)

    def setMainDictionary (self, sfDictionary):
        "returns True if the dictionary is loaded"
        self.oMainDic = self._loadDictionary(sfDictionary)
        return bool(self.oMainDic)
            
    def setExtendedDictionary (self, sfDictionary):
        "returns True if the dictionary is loaded"
        self.oExtendedDic = self._loadDictionary(sfDictionary)
        return bool(self.oExtendedDic)

    def setPersonalDictionary (self, sfDictionary):
        "returns True if the dictionary is loaded"
        self.oPersonalDic = self._loadDictionary(sfDictionary)
        return bool(self.oPersonalDic)

    # parse text functions

    def parseParagraph (self, sText, bSpellSugg=False):
        if not self.oTokenizer:
            self.loadTokenizer()
        aSpellErrs = []
        for dToken in self.oTokenizer.genTokens(sText):
            if dToken['sType'] == "WORD" and not self.isValidToken(dToken['sValue']):
                if bSpellSugg:
                    dToken['aSuggestions'] = []
                    for lSugg in self.suggest(dToken['sValue']):
                        dToken['aSuggestions'].extend(lSugg)
                aSpellErrs.append(dToken)
        return aSpellErrs

    def countWordsOccurrences (self, sText, bByLemma=False, bOnlyUnknownWords=False, dWord={}):
        if not self.oTokenizer:
            self.loadTokenizer()
        for dToken in self.oTokenizer.genTokens(sText):
            if dToken['sType'] == "WORD":
                if bOnlyUnknownWords:
                    if not self.isValidToken(dToken['sValue']):
                        dWord[dToken['sValue']] = dWord.get(dToken['sValue'], 0) + 1
                else:
                    if not bByLemma:
                        dWord[dToken['sValue']] = dWord.get(dToken['sValue'], 0) + 1
                    else:
                        for sLemma in self.getLemma(dToken['sValue']):
                            dWord[sLemma] = dWord.get(sLemma, 0) + 1
        return dWord

    # IBDAWG functions

    def isValidToken (self, sToken):
        "checks if sToken is valid (if there is hyphens in sToken, sToken is split, each part is checked)"
        if self.oMainDic.isValidToken(sToken):
            return True
96
97
98
99
100
101
102



103
104
105
106
107
108
109
        lResult = self.oMainDic.getMorph(sWord)
        if self.oExtendedDic:
            lResult.extend(self.oExtendedDic.getMorph(sWord))
        if self.oPersonalDic:
            lResult.extend(self.oPersonalDic.getMorph(sWord))
        return lResult




    def suggest (self, sWord, nSuggLimit=10):
        "generator: returns 1, 2 or 3 lists of suggestions"
        yield self.oMainDic.suggest(sWord, nSuggLimit)
        if self.oExtendedDic:
            yield self.oExtendedDic.suggest(sWord, nSuggLimit)
        if self.oPersonalDic:
            yield self.oPersonalDic.suggest(sWord, nSuggLimit)







>
>
>







131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
        lResult = self.oMainDic.getMorph(sWord)
        if self.oExtendedDic:
            lResult.extend(self.oExtendedDic.getMorph(sWord))
        if self.oPersonalDic:
            lResult.extend(self.oPersonalDic.getMorph(sWord))
        return lResult

    def getLemma (self, sWord):
        return set([ s[1:s.find(" ")]  for s in self.getMorph(sWord) ])

    def suggest (self, sWord, nSuggLimit=10):
        "generator: returns 1, 2 or 3 lists of suggestions"
        yield self.oMainDic.suggest(sWord, nSuggLimit)
        if self.oExtendedDic:
            yield self.oExtendedDic.suggest(sWord, nSuggLimit)
        if self.oPersonalDic:
            yield self.oPersonalDic.suggest(sWord, nSuggLimit)

Modified tests/fr/horla.res.txt from [036f6beeea] to [0417945727].

1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
Guy de Maupassant

Le Horla (1887)
   °°°°°
Le Horla, P. Ollendorff, 1895 [trente-cinquième édition] (pp. 3-68).
   °°°°°     °°°°°°°°°°


8 mai. – Quelle journée admirable ! J’ai passé toute la matinée étendu sur 
                                                                ^^^^^^

* 64:70  # 3514s  : Accord de genre erroné : « matinée » est féminin, « étendu »
  est masculin.
  > Suggestions : étendue

l’herbe, devant ma maison, sous l’énorme platane qui la couvre, l’abrite et 
l’ombrage tout entière. J’aime ce pays, et j’aime y vivre parce que j’y ai mes 
racines, ces profondes et délicates racines, qui attachent un homme à la terre 
où sont nés et morts ses aïeux, qui l’attachent à ce qu’on pense et à ce qu’on 
mange, aux usages comme aux nourritures, aux locutions locales, aux intonations 










>
|
<







1
2
3
4
5
6
7
8
9
10
11
12

13
14
15
16
17
18
19
Guy de Maupassant

Le Horla (1887)
   °°°°°
Le Horla, P. Ollendorff, 1895 [trente-cinquième édition] (pp. 3-68).
   °°°°°     °°°°°°°°°°


8 mai. – Quelle journée admirable ! J’ai passé toute la matinée étendu sur 
                                                                ^^^^^^
* 64:70  # 6628s / gn_2m_la:
  Accord de genre erroné : « matinée » est féminin, « étendu » est masculin.

  > Suggestions : étendue

l’herbe, devant ma maison, sous l’énorme platane qui la couvre, l’abrite et 
l’ombrage tout entière. J’aime ce pays, et j’aime y vivre parce que j’y ai mes 
racines, ces profondes et délicates racines, qui attachent un homme à la terre 
où sont nés et morts ses aïeux, qui l’attachent à ce qu’on pense et à ce qu’on 
mange, aux usages comme aux nourritures, aux locutions locales, aux intonations 
107
108
109
110
111
112
113

114
115
116
117
118
119
120
121
122
tomberait pour s’y noyer, dans un gouffre d’eau stagnante. Je ne le sens pas 
venir, comme autrefois, ce sommeil perfide, caché près de moi, qui me guette, 
qui va me saisir par la tête, me fermer les yeux, m’anéantir.

Je dors – longtemps – deux ou trois heures – puis un rêve – non – un cauchemar 
m’étreint. Je sens bien que je suis couché et que je dors,… je le sens et je le 
                                                         ^^

* 136:138  # 377p  : Typographie : pas de virgule avant les points de
  suspension.
  > Suggestions : …

sais… et je sens aussi que quelqu’un s’approche de moi, me regarde, me palpe, 
monte sur mon lit, s’agenouille sur ma poitrine, me prend le cou entre ses 
mains et serre… serre… de toute sa force pour m’étrangler.

Moi, je me débats, lié par cette impuissance atroce, qui nous paralyse dans les 







>
|
<







107
108
109
110
111
112
113
114
115

116
117
118
119
120
121
122
tomberait pour s’y noyer, dans un gouffre d’eau stagnante. Je ne le sens pas 
venir, comme autrefois, ce sommeil perfide, caché près de moi, qui me guette, 
qui va me saisir par la tête, me fermer les yeux, m’anéantir.

Je dors – longtemps – deux ou trois heures – puis un rêve – non – un cauchemar 
m’étreint. Je sens bien que je suis couché et que je dors,… je le sens et je le 
                                                         ^^
* 136:138  # 704p / virg_virgule_avant_points_suspension:
  Typographie : pas de virgule avant les points de suspension.

  > Suggestions : …

sais… et je sens aussi que quelqu’un s’approche de moi, me regarde, me palpe, 
monte sur mon lit, s’agenouille sur ma poitrine, me prend le cou entre ses 
mains et serre… serre… de toute sa force pour m’étrangler.

Moi, je me débats, lié par cette impuissance atroce, qui nous paralyse dans les 
475
476
477
478
479
480
481

482
483
484
485
486
487
488
489

— Oui, moi, ou plutôt mon mari, qui me charge de les trouver.

J’étais tellement stupéfait, que je balbutiais mes réponses. Je me demandais si 
vraiment elle ne s’était pas moquée de moi avec le docteur Parent, si ce 
n’était pas là une simple farce préparée d’avance et fort bien jouée.
                                ^^^^^^^^^^^^^^^^^

* 185:202  # 1518s  : Pléonasme.
  > Suggestions : préparée


Mais, en la regardant avec attention, tous mes doutes se dissipèrent. Elle 
tremblait d’angoisse, tant cette démarche lui était douloureuse, et je compris 
qu’elle avait la gorge pleine de sanglots.








>
|







475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490

— Oui, moi, ou plutôt mon mari, qui me charge de les trouver.

J’étais tellement stupéfait, que je balbutiais mes réponses. Je me demandais si 
vraiment elle ne s’était pas moquée de moi avec le docteur Parent, si ce 
n’était pas là une simple farce préparée d’avance et fort bien jouée.
                                ^^^^^^^^^^^^^^^^^
* 185:202  # 3020s / pleo_verbe_à_l_avance:
  Pléonasme.
  > Suggestions : préparée


Mais, en la regardant avec attention, tous mes doutes se dissipèrent. Elle 
tremblait d’angoisse, tant cette démarche lui était douloureuse, et je compris 
qu’elle avait la gorge pleine de sanglots.

584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600



601
602
603
604
605
606
607
Puis il la réveilla. Je tirai de ma poche un portefeuille :

— Voici, ma chère cousine, ce que vous m’avez demandé ce matin.

Elle fut tellement surprise que je n’osai pas insister. J’essayai cependant de 
ranimer sa mémoire, mais elle nia avec force, crut que je me moquais d’elle, et 
faillit, à la fin, se fâcher.
· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	·

Voilà ! je viens de rentrer ; et je n’ai pu déjeuner, tant cette expérience m’a 
bouleversé.

19 juillet. – Beaucoup de personnes à qui j’ai raconté cette aventure se sont 
moquées de moi. Je ne sais plus que penser. Le sage dit : Peut-être ?

21 juillet. – J’ai été dîner à Bougival, puis j’ai passé la soirée au bal des 
                               °°°°°°°°



canotiers. Décidément, tout dépend des lieux et des milieux. Croire au 
surnaturel dans l’île de la Grenouillère, serait le comble de la folie… mais au 
sommet du mont Saint-Michel ?… mais dans les Indes ? Nous subissons 
effroyablement l’influence de ce qui nous entoure. Je rentrerai chez moi la 
semaine prochaine.

30 juillet. – Je suis revenu dans ma maison depuis hier. Tout va bien.







|








|
>
>
>







585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
Puis il la réveilla. Je tirai de ma poche un portefeuille :

— Voici, ma chère cousine, ce que vous m’avez demandé ce matin.

Elle fut tellement surprise que je n’osai pas insister. J’essayai cependant de 
ranimer sa mémoire, mais elle nia avec force, crut que je me moquais d’elle, et 
faillit, à la fin, se fâcher.
· · · · · · · · · · · · · · · · · · · · · · · ·

Voilà ! je viens de rentrer ; et je n’ai pu déjeuner, tant cette expérience m’a 
bouleversé.

19 juillet. – Beaucoup de personnes à qui j’ai raconté cette aventure se sont 
moquées de moi. Je ne sais plus que penser. Le sage dit : Peut-être ?

21 juillet. – J’ai été dîner à Bougival, puis j’ai passé la soirée au bal des 
                ^^^^^^         °°°°°°°°
* 16:22  # 9891s / ppas_avoir_été:
  Tournure familière. Utilisez « être allé » plutôt que « avoir été ».

canotiers. Décidément, tout dépend des lieux et des milieux. Croire au 
surnaturel dans l’île de la Grenouillère, serait le comble de la folie… mais au 
sommet du mont Saint-Michel ?… mais dans les Indes ? Nous subissons 
effroyablement l’influence de ce qui nous entoure. Je rentrerai chez moi la 
semaine prochaine.

30 juillet. – Je suis revenu dans ma maison depuis hier. Tout va bien.
810
811
812
813
814
815
816

817
818

819
820

821
822
823
824
825
826
827
828
829
830
831
832
833
834
835

836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851

852


853
854
855
856
857
858
859
860
s’éteignit, et ma fenêtre se ferma comme si un malfaiteur surpris se fût élancé 
dans la nuit, en prenant à pleines mains les battants.

Donc, il s’était sauvé ; il avait eu peur, peur de moi, lui !

Alors,… alors… demain… ou après,… ou un jour quelconque,… je pourrai donc le 
     ^^                        ^^                      ^^

* 5:7  # 377p  : Typographie : pas de virgule avant les points de suspension.
  > Suggestions : …

* 31:33  # 377p  : Typographie : pas de virgule avant les points de suspension.
  > Suggestions : …

* 55:57  # 377p  : Typographie : pas de virgule avant les points de suspension.
  > Suggestions : …

tenir sous mes poings, et l’écraser contre le sol ! Est-ce que les chiens, 
quelquefois, ne mordent point et n’étranglent pas leurs maîtres ?

18 août. – J’ai songé toute la journée. Oh ! oui, je vais lui obéir, suivre ses 
impulsions, accomplir toutes ses volontés, me faire humble, soumis, lâche. Il 
est le plus fort. Mais une heure viendra…

19 août. – Je sais… je sais… je sais tout ! Je viens de lire ceci dans la Revue 
du Monde scientifique : « Une nouvelle assez curieuse nous arrive de Rio de 
Janeiro. Une folie, une épidémie de folie, comparable aux démences contagieuses 
qui atteignirent les peuples d’Europe au moyen âge, sévit en ce moment dans la 
                                         ^^^^^^^^^

* 277:286  # 5069s  : Le « Moyen Âge ».
  > Suggestions : Moyen Âge

province de San-Paulo. Les habitants éperdus quittent leurs maisons, désertent 
leurs villages, abandonnent leurs cultures, se disant poursuivis, possédés, 
gouvernés comme un bétail humain par des êtres invisibles bien que tangibles, 
des sortes de vampires qui se nourrissent de leur vie, pendant leur sommeil, et 
qui boivent en outre de l’eau et du lait sans paraître toucher à aucun autre 
aliment.

« M. le professeur Don Pedro Henriquez, accompagné de plusieurs savants 
                             °°°°°°°°°
médecins, est parti pour la province de San-Paulo, afin d’étudier sur place les 
origines et les manifestations de cette surprenante folie, et de proposer à 
l’Empereur les mesures qui lui paraîtront le plus propres à rappeler à la 
                                                  ^^^^^^^

* 278:285  # 3142s  : Accord de nombre erroné : « propres » devrait être au


  singulier.
  > Suggestions : propre

raison ces populations en délire. »

Ah ! Ah ! je me rappelle, je me rappelle le beau trois-mâts brésilien qui passa 
sous mes fenêtres en remontant la Seine, le 8 mai dernier ! Je le trouvai si 
joli, si blanc, si gai ! L’Être était dessus, venant de là-bas, où sa race est 







>
|

>
|

>
|














>
|














|
>
|
>
>
|







814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
s’éteignit, et ma fenêtre se ferma comme si un malfaiteur surpris se fût élancé 
dans la nuit, en prenant à pleines mains les battants.

Donc, il s’était sauvé ; il avait eu peur, peur de moi, lui !

Alors,… alors… demain… ou après,… ou un jour quelconque,… je pourrai donc le 
     ^^                        ^^                      ^^
* 5:7  # 704p / virg_virgule_avant_points_suspension:
  Typographie : pas de virgule avant les points de suspension.
  > Suggestions : …
* 31:33  # 704p / virg_virgule_avant_points_suspension:
  Typographie : pas de virgule avant les points de suspension.
  > Suggestions : …
* 55:57  # 704p / virg_virgule_avant_points_suspension:
  Typographie : pas de virgule avant les points de suspension.
  > Suggestions : …

tenir sous mes poings, et l’écraser contre le sol ! Est-ce que les chiens, 
quelquefois, ne mordent point et n’étranglent pas leurs maîtres ?

18 août. – J’ai songé toute la journée. Oh ! oui, je vais lui obéir, suivre ses 
impulsions, accomplir toutes ses volontés, me faire humble, soumis, lâche. Il 
est le plus fort. Mais une heure viendra…

19 août. – Je sais… je sais… je sais tout ! Je viens de lire ceci dans la Revue 
du Monde scientifique : « Une nouvelle assez curieuse nous arrive de Rio de 
Janeiro. Une folie, une épidémie de folie, comparable aux démences contagieuses 
qui atteignirent les peuples d’Europe au moyen âge, sévit en ce moment dans la 
                                         ^^^^^^^^^
* 277:286  # 8968s / maj_Moyen_Âge:
  Le « Moyen Âge ».
  > Suggestions : Moyen Âge

province de San-Paulo. Les habitants éperdus quittent leurs maisons, désertent 
leurs villages, abandonnent leurs cultures, se disant poursuivis, possédés, 
gouvernés comme un bétail humain par des êtres invisibles bien que tangibles, 
des sortes de vampires qui se nourrissent de leur vie, pendant leur sommeil, et 
qui boivent en outre de l’eau et du lait sans paraître toucher à aucun autre 
aliment.

« M. le professeur Don Pedro Henriquez, accompagné de plusieurs savants 
                             °°°°°°°°°
médecins, est parti pour la province de San-Paulo, afin d’étudier sur place les 
origines et les manifestations de cette surprenante folie, et de proposer à 
l’Empereur les mesures qui lui paraîtront le plus propres à rappeler à la 
                                          ^^      ^^^^^^^
* 270:272  # 5835s / gn_le_accord2:
  Accord de nombre erroné : « propres » est au pluriel.
  > Suggestions : les
* 278:285  # 5835s / gn_le_accord2:
  Accord de nombre erroné : « propres » devrait être au singulier.
  > Suggestions : propre

raison ces populations en délire. »

Ah ! Ah ! je me rappelle, je me rappelle le beau trois-mâts brésilien qui passa 
sous mes fenêtres en remontant la Seine, le 8 mai dernier ! Je le trouvai si 
joli, si blanc, si gai ! L’Être était dessus, venant de là-bas, où sa race est 
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949

Mais direz-vous, le papillon ! une fleur qui vole ! J’en rêve un qui serait 
grand comme cent univers, avec des ailes dont je ne puis même exprimer la 
forme, la beauté, la couleur et le mouvement. Mais je le vois… il va d’étoile 
en étoile, les rafraîchissant et les embaumant au souffle harmonieux et léger 
de sa course !… Et les peuples de là-haut le regardent passer, extasiés et 
ravis !…
· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	·

Qu’ai-je donc ? C’est lui, lui, le Horla, qui me hante, qui me fait penser ces 
                                   °°°°°
folies ! Il est en moi, il devient mon âme ; je le tuerai !

19 août. – Je le tuerai. Je l’ai vu ! je me suis assis hier soir, à ma table ; 
et je fis semblant d’écrire avec une grande attention. Je savais bien qu’il 







|







946
947
948
949
950
951
952
953
954
955
956
957
958
959
960

Mais direz-vous, le papillon ! une fleur qui vole ! J’en rêve un qui serait 
grand comme cent univers, avec des ailes dont je ne puis même exprimer la 
forme, la beauté, la couleur et le mouvement. Mais je le vois… il va d’étoile 
en étoile, les rafraîchissant et les embaumant au souffle harmonieux et léger 
de sa course !… Et les peuples de là-haut le regardent passer, extasiés et 
ravis !…
· · · · · · · · · · · · · · · · · · · · · · · ·

Qu’ai-je donc ? C’est lui, lui, le Horla, qui me hante, qui me fait penser ces 
                                   °°°°°
folies ! Il est en moi, il devient mon âme ; je le tuerai !

19 août. – Je le tuerai. Je l’ai vu ! je me suis assis hier soir, à ma table ; 
et je fis semblant d’écrire avec une grande attention. Je savais bien qu’il 
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
alors ?…

21 août. – J’ai fait venir un serrurier de Rouen, et lui ai commandé pour ma 
chambre des persiennes de fer, comme en ont, à Paris, certains hôtels 
particuliers, au rez-de-chaussée, par crainte des voleurs. Il me fera, en 
outre, une porte pareille. Je me suis donné pour un poltron, mais je m’en 
moque !…
· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	·

10 septembre. – Rouen, hôtel continental. C’est fait… c’est fait… mais est-il 
mort ? J’ai l’âme bouleversée de ce que j’ai vu.

Hier donc, le serrurier ayant posé ma persienne et ma porte de fer, j’ai laissé 
tout ouvert jusqu’à minuit, bien qu’il commençât à faire froid.








|







1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
alors ?…

21 août. – J’ai fait venir un serrurier de Rouen, et lui ai commandé pour ma 
chambre des persiennes de fer, comme en ont, à Paris, certains hôtels 
particuliers, au rez-de-chaussée, par crainte des voleurs. Il me fera, en 
outre, une porte pareille. Je me suis donné pour un poltron, mais je m’en 
moque !…
· · · · · · · · · · · · · · · · · · · · · · · ·

10 septembre. – Rouen, hôtel continental. C’est fait… c’est fait… mais est-il 
mort ? J’ai l’âme bouleversée de ce que j’ai vu.

Hier donc, le serrurier ayant posé ma persienne et ma porte de fer, j’ai laissé 
tout ouvert jusqu’à minuit, bien qu’il commençât à faire froid.

1024
1025
1026
1027
1028
1029
1030

1031
1032
1033
1034
1035
1036
1037
1038
1039
bien refermé, à double tour, la grande porte d’entrée.

Et j’allai me cacher au fond de mon jardin, dans un massif de lauriers. Comme 
ce fut long ! comme ce fut long ! Tout était noir, muet, immobile ; pas un 
souffle d’air, pas une étoile, des montagnes de nuages qu’on ne voyait point, 
mais qui pesaient sur mon âme si lourds, si lourds.
                                 ^^^^^^

* 264:270  # 3554s  : Accord de nombre erroné avec « âme » : « lourds » devrait
  être au singulier.
  > Suggestions : lourd


Je regardais ma maison, et j’attendais. Comme ce fut long ! Je croyais déjà que 
le feu s’était éteint tout seul, ou qu’il l’avait éteint, Lui, quand une des 
fenêtres d’en bas creva sous la poussée de l’incendie, et une flamme, une 
grande flamme rouge et jaune, longue, molle, caressante, monta le long du mur 







>
|
<







1035
1036
1037
1038
1039
1040
1041
1042
1043

1044
1045
1046
1047
1048
1049
1050
bien refermé, à double tour, la grande porte d’entrée.

Et j’allai me cacher au fond de mon jardin, dans un massif de lauriers. Comme 
ce fut long ! comme ce fut long ! Tout était noir, muet, immobile ; pas un 
souffle d’air, pas une étoile, des montagnes de nuages qu’on ne voyait point, 
mais qui pesaient sur mon âme si lourds, si lourds.
                                 ^^^^^^
* 264:270  # 6602s / gn_2m_mon_ton_son:
  Accord de nombre erroné avec « âme » : « lourds » devrait être au singulier.

  > Suggestions : lourd


Je regardais ma maison, et j’attendais. Comme ce fut long ! Je croyais déjà que 
le feu s’était éteint tout seul, ou qu’il l’avait éteint, Lui, quand une des 
fenêtres d’en bas creva sous la poussée de l’incendie, et une flamme, une 
grande flamme rouge et jaune, longue, molle, caressante, monta le long du mur 

Modified tests/fr/horla.txt from [4cbaa54e8b] to [44498933b9].

291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
— Votre mari n’a plus besoin de cinq mille francs ! Vous allez donc oublier que vous avez prié votre cousin de vous les prêter, et, s’il vous parle de cela, vous ne comprendrez pas.

Puis il la réveilla. Je tirai de ma poche un portefeuille :

— Voici, ma chère cousine, ce que vous m’avez demandé ce matin.

Elle fut tellement surprise que je n’osai pas insister. J’essayai cependant de ranimer sa mémoire, mais elle nia avec force, crut que je me moquais d’elle, et faillit, à la fin, se fâcher.
· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	·

Voilà ! je viens de rentrer ; et je n’ai pu déjeuner, tant cette expérience m’a bouleversé.

19 juillet. – Beaucoup de personnes à qui j’ai raconté cette aventure se sont moquées de moi. Je ne sais plus que penser. Le sage dit : Peut-être ?

21 juillet. – J’ai été dîner à Bougival, puis j’ai passé la soirée au bal des canotiers. Décidément, tout dépend des lieux et des milieux. Croire au surnaturel dans l’île de la Grenouillère, serait le comble de la folie… mais au sommet du mont Saint-Michel ?… mais dans les Indes ? Nous subissons effroyablement l’influence de ce qui nous entoure. Je rentrerai chez moi la semaine prochaine.








|







291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
— Votre mari n’a plus besoin de cinq mille francs ! Vous allez donc oublier que vous avez prié votre cousin de vous les prêter, et, s’il vous parle de cela, vous ne comprendrez pas.

Puis il la réveilla. Je tirai de ma poche un portefeuille :

— Voici, ma chère cousine, ce que vous m’avez demandé ce matin.

Elle fut tellement surprise que je n’osai pas insister. J’essayai cependant de ranimer sa mémoire, mais elle nia avec force, crut que je me moquais d’elle, et faillit, à la fin, se fâcher.
· · · · · · · · · · · · · · · · · · · · · · · ·

Voilà ! je viens de rentrer ; et je n’ai pu déjeuner, tant cette expérience m’a bouleversé.

19 juillet. – Beaucoup de personnes à qui j’ai raconté cette aventure se sont moquées de moi. Je ne sais plus que penser. Le sage dit : Peut-être ?

21 juillet. – J’ai été dîner à Bougival, puis j’ai passé la soirée au bal des canotiers. Décidément, tout dépend des lieux et des milieux. Croire au surnaturel dans l’île de la Grenouillère, serait le comble de la folie… mais au sommet du mont Saint-Michel ?… mais dans les Indes ? Nous subissons effroyablement l’influence de ce qui nous entoure. Je rentrerai chez moi la semaine prochaine.

404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
Un être nouveau ! pourquoi pas ? Il devait venir assurément ! pourquoi serions-nous les derniers ! Nous ne le distinguons point, ainsi que tous les autres créés avant nous ? C’est que sa nature est plus parfaite, son corps plus fin et plus fini que le nôtre, que le nôtre si faible, si maladroitement conçu, encombré d’organes toujours fatigués, toujours forcés comme des ressorts trop complexes, que le nôtre, qui vit comme une plante et comme une bête, en se nourrissant péniblement d’air, d’herbe et de viande, machine animale en proie aux maladies, aux déformations, aux putréfactions, poussive, mal réglée, naïve et bizarre, ingénieusement mal faite, œuvre grossière et délicate, ébauche d’être qui pourrait devenir intelligent et superbe.

Nous sommes quelques-uns, si peu sur ce monde, depuis l’huître jusqu’à l’homme. Pourquoi pas un de plus, une fois accomplie la période qui sépare les apparitions successives de toutes les espèces diverses ?

Pourquoi pas un de plus ? Pourquoi pas aussi d’autres arbres aux fleurs immenses, éclatantes et parfumant des régions entières ? Pourquoi pas d’autres éléments que le feu, l’air, la terre et l’eau ? – Ils sont quatre, rien que quatre, ces pères nourriciers des êtres ! Quelle pitié ! Pourquoi ne sont-ils pas quarante, quatre cents, quatre mille ! Comme tout est pauvre, mesquin, misérable ! avarement donné, sèchement inventé, lourdement fait ! Ah ! l’éléphant, l’hippopotame, que de grâce ! Le chameau que d’élégance !

Mais direz-vous, le papillon ! une fleur qui vole ! J’en rêve un qui serait grand comme cent univers, avec des ailes dont je ne puis même exprimer la forme, la beauté, la couleur et le mouvement. Mais je le vois… il va d’étoile en étoile, les rafraîchissant et les embaumant au souffle harmonieux et léger de sa course !… Et les peuples de là-haut le regardent passer, extasiés et ravis !…
· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	·

Qu’ai-je donc ? C’est lui, lui, le Horla, qui me hante, qui me fait penser ces folies ! Il est en moi, il devient mon âme ; je le tuerai !

19 août. – Je le tuerai. Je l’ai vu ! je me suis assis hier soir, à ma table ; et je fis semblant d’écrire avec une grande attention. Je savais bien qu’il viendrait rôder autour de moi, tout près, si près que je pourrais peut-être le toucher, le saisir ? Et alors !… alors, j’aurais la force des désespérés ; j’aurais mes mains, mes genoux, ma poitrine, mon front, mes dents pour l’étrangler, l’écraser, le mordre, le déchirer.

Et je le guettais avec tous mes organes surexcités.








|







404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
Un être nouveau ! pourquoi pas ? Il devait venir assurément ! pourquoi serions-nous les derniers ! Nous ne le distinguons point, ainsi que tous les autres créés avant nous ? C’est que sa nature est plus parfaite, son corps plus fin et plus fini que le nôtre, que le nôtre si faible, si maladroitement conçu, encombré d’organes toujours fatigués, toujours forcés comme des ressorts trop complexes, que le nôtre, qui vit comme une plante et comme une bête, en se nourrissant péniblement d’air, d’herbe et de viande, machine animale en proie aux maladies, aux déformations, aux putréfactions, poussive, mal réglée, naïve et bizarre, ingénieusement mal faite, œuvre grossière et délicate, ébauche d’être qui pourrait devenir intelligent et superbe.

Nous sommes quelques-uns, si peu sur ce monde, depuis l’huître jusqu’à l’homme. Pourquoi pas un de plus, une fois accomplie la période qui sépare les apparitions successives de toutes les espèces diverses ?

Pourquoi pas un de plus ? Pourquoi pas aussi d’autres arbres aux fleurs immenses, éclatantes et parfumant des régions entières ? Pourquoi pas d’autres éléments que le feu, l’air, la terre et l’eau ? – Ils sont quatre, rien que quatre, ces pères nourriciers des êtres ! Quelle pitié ! Pourquoi ne sont-ils pas quarante, quatre cents, quatre mille ! Comme tout est pauvre, mesquin, misérable ! avarement donné, sèchement inventé, lourdement fait ! Ah ! l’éléphant, l’hippopotame, que de grâce ! Le chameau que d’élégance !

Mais direz-vous, le papillon ! une fleur qui vole ! J’en rêve un qui serait grand comme cent univers, avec des ailes dont je ne puis même exprimer la forme, la beauté, la couleur et le mouvement. Mais je le vois… il va d’étoile en étoile, les rafraîchissant et les embaumant au souffle harmonieux et léger de sa course !… Et les peuples de là-haut le regardent passer, extasiés et ravis !…
· · · · · · · · · · · · · · · · · · · · · · · ·

Qu’ai-je donc ? C’est lui, lui, le Horla, qui me hante, qui me fait penser ces folies ! Il est en moi, il devient mon âme ; je le tuerai !

19 août. – Je le tuerai. Je l’ai vu ! je me suis assis hier soir, à ma table ; et je fis semblant d’écrire avec une grande attention. Je savais bien qu’il viendrait rôder autour de moi, tout près, si près que je pourrais peut-être le toucher, le saisir ? Et alors !… alors, j’aurais la force des désespérés ; j’aurais mes mains, mes genoux, ma poitrine, mon front, mes dents pour l’étrangler, l’écraser, le mordre, le déchirer.

Et je le guettais avec tous mes organes surexcités.

429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
Je pus enfin me distinguer complètement, ainsi que je le fais chaque jour en me regardant.

Je l’avais vu ! L’épouvante m’en est restée, qui me fait encore frissonner.

20 août. – Le tuer, comment ? puisque je ne peux l’atteindre ? Le poison ? mais il me verrait le mêler à l’eau ; et nos poisons, d’ailleurs, auraient-ils un effet sur son corps imperceptible ? Non… non… sans aucun doute… Alors ?… alors ?…

21 août. – J’ai fait venir un serrurier de Rouen, et lui ai commandé pour ma chambre des persiennes de fer, comme en ont, à Paris, certains hôtels particuliers, au rez-de-chaussée, par crainte des voleurs. Il me fera, en outre, une porte pareille. Je me suis donné pour un poltron, mais je m’en moque !…
· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	· 	·

10 septembre. – Rouen, hôtel continental. C’est fait… c’est fait… mais est-il mort ? J’ai l’âme bouleversée de ce que j’ai vu.

Hier donc, le serrurier ayant posé ma persienne et ma porte de fer, j’ai laissé tout ouvert jusqu’à minuit, bien qu’il commençât à faire froid.

Tout à coup, j’ai senti qu’il était là, et une joie, une joie folle m’a saisi. Je me suis levé lentement, et j’ai marché à droite, à gauche, longtemps pour qu’il ne devinât rien ; puis j’ai ôté mes bottines et mis mes savates avec négligence ; puis j’ai fermé ma persienne de fer, et revenant à pas tranquilles vers la porte, j’ai fermé la porte aussi à double tour. Retournant alors vers la fenêtre, je la fixai par un cadenas, dont je mis la clef dans ma poche.








|







429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
Je pus enfin me distinguer complètement, ainsi que je le fais chaque jour en me regardant.

Je l’avais vu ! L’épouvante m’en est restée, qui me fait encore frissonner.

20 août. – Le tuer, comment ? puisque je ne peux l’atteindre ? Le poison ? mais il me verrait le mêler à l’eau ; et nos poisons, d’ailleurs, auraient-ils un effet sur son corps imperceptible ? Non… non… sans aucun doute… Alors ?… alors ?…

21 août. – J’ai fait venir un serrurier de Rouen, et lui ai commandé pour ma chambre des persiennes de fer, comme en ont, à Paris, certains hôtels particuliers, au rez-de-chaussée, par crainte des voleurs. Il me fera, en outre, une porte pareille. Je me suis donné pour un poltron, mais je m’en moque !…
· · · · · · · · · · · · · · · · · · · · · · · ·

10 septembre. – Rouen, hôtel continental. C’est fait… c’est fait… mais est-il mort ? J’ai l’âme bouleversée de ce que j’ai vu.

Hier donc, le serrurier ayant posé ma persienne et ma porte de fer, j’ai laissé tout ouvert jusqu’à minuit, bien qu’il commençât à faire froid.

Tout à coup, j’ai senti qu’il était là, et une joie, une joie folle m’a saisi. Je me suis levé lentement, et j’ai marché à droite, à gauche, longtemps pour qu’il ne devinât rien ; puis j’ai ôté mes bottines et mis mes savates avec négligence ; puis j’ai fermé ma persienne de fer, et revenant à pas tranquilles vers la porte, j’ai fermé la porte aussi à double tour. Retournant alors vers la fenêtre, je la fixai par un cadenas, dont je mis la clef dans ma poche.

Modified tests/fr/text2.res.txt from [4c753a53e0] to [56d437d0e0].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{ "grammalecte": "0.5.12", "lang": "fr", "data" : [
{"lSpellingErrors": [], "lGrammarErrors": [{"aSuggestions": ["Lorsqu’"], "sType": "apos", "nStartX": 0, "sRuleId": "apostrophe_typographique", "sBefore": "", "nEndX": 7, "nStartY": 1, "sLineId": "523p", "nEndY": 1, "sMessage": "Apostrophe typographique.", "sUnderlined": "Lorsqu'", "sAfter": "il arrivas", "URL": ""}, {"aSuggestions": ["arriva"], "sType": "gv", "nStartX": 10, "sRuleId": "conj_il", "sBefore": "Lorsqu'il ", "nEndX": 17, "nStartY": 1, "sLineId": "7710s", "nEndY": 1, "sMessage": "Conjugaison erronée. Accord avec « il ». Le verbe devrait être à la 3ᵉ personne du singulier.", "sUnderlined": "arrivas", "sAfter": "", "URL": ""}, {"aSuggestions": ["arriva"], "sType": "gv", "nStartX": 10, "sRuleId": "conj_xxxas_sans_sujet", "sBefore": "Lorsqu'il ", "nEndX": 17, "nStartY": 1, "sLineId": "7959s", "nEndY": 1, "sMessage": "Incohérence. Ceci est un verbe à la 2ᵉ personne du singulier. Sujet (“tu” ou “toi qui”) introuvable.", "sUnderlined": "arrivas", "sAfter": "", "URL": ""}]},
{"lSpellingErrors": [], "lGrammarErrors": []},
{"lSpellingErrors": [{"sType": "WORD", "nStartX": 3, "nEndX": 8, "nEndY": 5, "nStartY": 5, "sValue": "Horla"}, {"sType": "WORD", "nStartX": 3, "nEndX": 8, "nEndY": 6, "nStartY": 6, "sValue": "Horla"}, {"sType": "WORD", "nStartX": 13, "nEndX": 23, "nEndY": 6, "nStartY": 6, "sValue": "Ollendorff"}], "lGrammarErrors": []},
{"lSpellingErrors": [], "lGrammarErrors": [{"aSuggestions": ["J’"], "sType": "apos", "nStartX": 37, "sRuleId": "apostrophe_typographique", "sBefore": "8 mai. – Quelle journées admirable ! ", "nEndX": 39, "nStartY": 9, "sLineId": "523p", "nEndY": 9, "sMessage": "Apostrophe typographique.", "sUnderlined": "J'", "sAfter": "ai passés toute la matinée étendu sur l’herbe, devant ma maison, sous l’énorme p", "URL": ""}, {"aSuggestions": ["Quelles"], "sType": "gn", "nStartX": 9, "sRuleId": "gn_quelle_accord", "sBefore": "– ", "nEndX": 15, "nStartY": 9, "sLineId": "3673s", "nEndY": 9, "sMessage": "Accord de nombre erroné : « journées » est au pluriel.", "sUnderlined": "Quelle", "sAfter": " journées admirable ! ", "URL": ""}, {"aSuggestions": ["étendue"], "sType": "gn", "nStartX": 66, "sRuleId": "gn_2m_det_fem_sing", "sBefore": "J'ai passés toute la matinée ", "nEndX": 72, "nStartY": 9, "sLineId": "3990s", "nEndY": 9, "sMessage": "Accord de genre erroné : « matinée » est féminin, « étendu » est masculin.", "sUnderlined": "étendu", "sAfter": " sur l’herbe, devant ma maison, sous l’énorme platane qui la couvre, l’abrite et", "URL": ""}, {"aSuggestions": ["passé"], "sType": "ppas", "nStartX": 42, "sRuleId": "ppas_pronom_avoir", "sBefore": "J'ai ", "nEndX": 48, "nStartY": 9, "sLineId": "7013s", "nEndY": 9, "sMessage": "Ce verbe devrait être un participe passé au masculin singulier.", "sUnderlined": "passés", "sAfter": " toute la matinée étendu sur l’herbe, devant ma maison, sous l’énorme platane qu", "URL": "http://fr.wikipedia.org/wiki/Accord_du_participe_pass%C3%A9_en_fran%C3%A7ais"}]},
{"lSpellingErrors": [], "lGrammarErrors": [{"aSuggestions": ["aime"], "sType": "gv", "nStartX": 2, "sRuleId": "conj_j", "sBefore": "J’", "nEndX": 7, "nStartY": 17, "sLineId": "7653s", "nEndY": 17, "sMessage": "Conjugaison erronée. Accord avec « je ». Le verbe devrait être à la 1ʳᵉ personne du singulier.", "sUnderlined": "aimes", "sAfter": " ma maison où j’ai grandi. ", "URL": ""}, {"aSuggestions": ["aime"], "sType": "gv", "nStartX": 2, "sRuleId": "conj_xxxes_sans_sujet", "sBefore": "J’", "nEndX": 7, "nStartY": 17, "sLineId": "7953s", "nEndY": 17, "sMessage": "Incohérence. Ceci est un verbe à la 2ᵉ personne du singulier. Sujet (“tu” ou “toi qui”) introuvable.", "sUnderlined": "aimes", "sAfter": " ma maison où j’ai grandi. ", "URL": ""}]},
{"lSpellingErrors": [], "lGrammarErrors": []},
{"lSpellingErrors": [], "lGrammarErrors": []},
{"lSpellingErrors": [], "lGrammarErrors": [{"aSuggestions": ["heures"], "sType": "gn", "nStartX": 10, "sRuleId": "gn_nombre_lettres_accord", "sBefore": "Vers onze ", "nEndX": 15, "nStartY": 30, "sLineId": "3626s", "nEndY": 30, "sMessage": "Accord de nombre erroné avec « onze » : « heure » devrait être au pluriel.", "sUnderlined": "heure", "sAfter": ", un long convoi de navires, traînés par un remorqueur, gros comme une mouche, e", "URL": ""}]},
{"lSpellingErrors": [{"sType": "WORD", "nStartX": 11, "nEndX": 20, "nEndY": 34, "nStartY": 34, "sValue": "goëlettes"}], "lGrammarErrors": [{"aSuggestions": ["saluais"], "sType": "gv", "nStartX": 15, "sRuleId": "conj_je_pronom", "sBefore": "Je le ", "nEndX": 22, "nStartY": 36, "sLineId": "7661s", "nEndY": 36, "sMessage": "Conjugaison erronée. Accord avec « Je ». Le verbe devrait être à la 1ʳᵉ personne du singulier.", "sUnderlined": "saluait", "sAfter": ", je ne sais pourquoi, tant ce navire me fit plaisir à voir.", "URL": ""}]},
{"lSpellingErrors": [], "lGrammarErrors": [{"aSuggestions": ["jours"], "sType": "gn", "nStartX": 76, "sRuleId": "gn_det_pluriel_accord", "sBefore": "– J’ai un peu de fièvre depuis quelques ", "nEndX": 80, "nStartY": 39, "sLineId": "3540s", "nEndY": 39, "sMessage": "Accord de nombre erroné : « jour » devrait être au pluriel.", "sUnderlined": "jour", "sAfter": ", et toi ; ", "URL": ""}]},
{"lSpellingErrors": [], "lGrammarErrors": [{"aSuggestions": ["confiance"], "sType": "gn", "nStartX": 17, "sRuleId": "gn_notre_votre_chaque_accord", "sBefore": "es influences mystérieuses qui changent en découragement notre bonheur et notre ", "nEndX": 27, "nStartY": 43, "sLineId": "3492s", "nEndY": 43, "sMessage": "Accord de nombre erroné : « confiances » devrait être au singulier.", "sUnderlined": "confiances", "sAfter": " en détresse. ", "URL": ""}, {"aSuggestions": ["descends"], "sType": "gv", "nStartX": 25, "sRuleId": "conj_je", "sBefore": "– Je ", "nEndX": 32, "nStartY": 46, "sLineId": "7657s", "nEndY": 46, "sMessage": "Conjugaison erronée. Accord avec « Je ». Le verbe devrait être à la 1ʳᵉ personne du singulier.", "sUnderlined": "descend", "sAfter": " le long de l’eau ; ", "URL": ""}]},
{"lSpellingErrors": [], "lGrammarErrors": [{"aSuggestions": ["Va"], "sType": "imp", "nStartX": 0, "sRuleId": "imp_va", "sBefore": "", "nEndX": 3, "nStartY": 57, "sLineId": "7357s", "nEndY": 57, "sMessage": "S’il s’agit d’un impératif, pas de “s”.", "sUnderlined": "Vas", "sAfter": " en enfer. ", "URL": ""}, {"aSuggestions": ["Va"], "sType": "gv", "nStartX": 0, "sRuleId": "conj_xxxas_sans_sujet", "sBefore": "", "nEndX": 3, "nStartY": 57, "sLineId": "7959s", "nEndY": 57, "sMessage": "Incohérence. Ceci est un verbe à la 2ᵉ personne du singulier. Sujet (“tu” ou “toi qui”) introuvable.", "sUnderlined": "Vas", "sAfter": " en enfer. ", "URL": ""}, {"aSuggestions": ["Va"], "sType": "imp", "nStartX": 0, "sRuleId": "imp_va", "sBefore": "", "nEndX": 3, "nStartY": 58, "sLineId": "7357s", "nEndY": 58, "sMessage": "S’il s’agit d’un impératif, pas de “s”.", "sUnderlined": "Vas", "sAfter": " en enfer.", "URL": ""}, {"aSuggestions": ["Va"], "sType": "gv", "nStartX": 0, "sRuleId": "conj_xxxas_sans_sujet", "sBefore": "", "nEndX": 3, "nStartY": 58, "sLineId": "7959s", "nEndY": 58, "sMessage": "Incohérence. Ceci est un verbe à la 2ᵉ personne du singulier. Sujet (“tu” ou “toi qui”) introuvable.", "sUnderlined": "Vas", "sAfter": " en enfer.", "URL": ""}]}
]}
|
|
|
|
|
|
|
|
|
|
|
|
|

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{ "grammalecte": "0.6.2", "lang": "fr", "data" : [
{"lGrammarErrors": [{"sLineId": "885p", "sRuleId": "apostrophe_typographique", "sType": "apos", "aSuggestions": ["Lorsqu’"], "sMessage": "Apostrophe typographique.", "URL": "", "nEndY": 1, "nEndX": 7, "nStartY": 1, "nStartX": 0}, {"sLineId": "11404s", "sRuleId": "conj_il", "sType": "conj", "aSuggestions": ["arriva"], "sMessage": "Conjugaison erronée. Accord avec « il ». Le verbe devrait être à la 3ᵉ personne du singulier.", "URL": "", "nEndY": 1, "nEndX": 17, "nStartY": 1, "nStartX": 10}], "lSpellingErrors": []},
{"lGrammarErrors": [], "lSpellingErrors": []},
{"lGrammarErrors": [], "lSpellingErrors": [{"sType": "WORD", "sValue": "Horla", "nEndY": 5, "nEndX": 8, "nStartY": 5, "nStartX": 3}, {"sType": "WORD", "sValue": "Horla", "nEndY": 6, "nEndX": 8, "nStartY": 6, "nStartX": 3}, {"sType": "WORD", "sValue": "Ollendorff", "nEndY": 6, "nEndX": 23, "nStartY": 6, "nStartX": 13}]},
{"lGrammarErrors": [{"sLineId": "885p", "sRuleId": "apostrophe_typographique", "sType": "apos", "aSuggestions": ["J’"], "sMessage": "Apostrophe typographique.", "URL": "", "nEndY": 9, "nEndX": 39, "nStartY": 9, "nStartX": 37}, {"sLineId": "6265s", "sRuleId": "gn_quelle_accord", "sType": "gn", "aSuggestions": ["Quelles"], "sMessage": "Accord de nombre erroné : « journées » est au pluriel.", "URL": "", "nEndY": 9, "nEndX": 15, "nStartY": 9, "nStartX": 9}, {"sLineId": "6265s", "sRuleId": "gn_quelle_accord", "sType": "gn", "aSuggestions": ["journée"], "sMessage": "Accord de nombre erroné : « Quelle » est au singulier.", "URL": "", "nEndY": 9, "nEndX": 24, "nStartY": 9, "nStartX": 16}, {"sLineId": "6628s", "sRuleId": "gn_2m_la", "sType": "gn", "aSuggestions": ["étendue"], "sMessage": "Accord de genre erroné : « matinée » est féminin, « étendu » est masculin.", "URL": "", "nEndY": 9, "nEndX": 72, "nStartY": 9, "nStartX": 66}, {"sLineId": "10365s", "sRuleId": "ppas_pronom_avoir", "sType": "ppas", "aSuggestions": ["passé"], "sMessage": "Ce verbe devrait être un participe passé au masculin singulier.", "URL": "http://fr.wikipedia.org/wiki/Accord_du_participe_pass%C3%A9_en_fran%C3%A7ais", "nEndY": 9, "nEndX": 48, "nStartY": 9, "nStartX": 42}], "lSpellingErrors": []},
{"lGrammarErrors": [{"sLineId": "11348s", "sRuleId": "conj_j", "sType": "conj", "aSuggestions": ["aime"], "sMessage": "Conjugaison erronée. Accord avec « je ». Le verbe devrait être à la 1ʳᵉ personne du singulier.", "URL": "", "nEndY": 17, "nEndX": 7, "nStartY": 17, "nStartX": 2}], "lSpellingErrors": []},
{"lGrammarErrors": [], "lSpellingErrors": []},
{"lGrammarErrors": [], "lSpellingErrors": []},
{"lGrammarErrors": [{"sLineId": "6199s", "sRuleId": "gn_nombre_lettres_accord", "sType": "gn", "aSuggestions": ["heures"], "sMessage": "Accord de nombre erroné avec « onze » : « heure » devrait être au pluriel.", "URL": "", "nEndY": 30, "nEndX": 15, "nStartY": 30, "nStartX": 10}], "lSpellingErrors": []},
{"lGrammarErrors": [{"sLineId": "11353s", "sRuleId": "conj_je", "sType": "conj", "aSuggestions": ["saluais"], "sMessage": "Conjugaison erronée. Accord avec « Je ». Le verbe devrait être à la 1ʳᵉ personne du singulier.", "URL": "", "nEndY": 36, "nEndX": 22, "nStartY": 36, "nStartX": 15}], "lSpellingErrors": [{"sType": "WORD", "sValue": "goëlettes", "nEndY": 34, "nEndX": 20, "nStartY": 34, "nStartX": 11}]},
{"lGrammarErrors": [{"sLineId": "6108s", "sRuleId": "gn_det_pluriel_accord", "sType": "gn", "aSuggestions": ["jours"], "sMessage": "Accord de nombre erroné : « jour » devrait être au pluriel.", "URL": "", "nEndY": 39, "nEndX": 80, "nStartY": 39, "nStartX": 76}], "lSpellingErrors": []},
{"lGrammarErrors": [{"sLineId": "6053s", "sRuleId": "gn_notre_votre_chaque_accord", "sType": "gn", "aSuggestions": ["confiance"], "sMessage": "Accord de nombre erroné : « confiances » devrait être au singulier.", "URL": "", "nEndY": 43, "nEndX": 27, "nStartY": 43, "nStartX": 17}, {"sLineId": "11353s", "sRuleId": "conj_je", "sType": "conj", "aSuggestions": ["descends"], "sMessage": "Conjugaison erronée. Accord avec « Je ». Le verbe devrait être à la 1ʳᵉ personne du singulier.", "URL": "", "nEndY": 46, "nEndX": 32, "nStartY": 46, "nStartX": 25}, {"sLineId": "12117s", "sRuleId": "inte_ce", "sType": "inte", "aSuggestions": ["est"], "sMessage": "Forme interrogative : « Es » n’est pas un verbe à la 3ᵉ personne du singulier.", "URL": "http://bdl.oqlf.gouv.qc.ca/bdl/gabarit_bdl.asp?id=4132", "nEndY": 48, "nEndX": 22, "nStartY": 48, "nStartX": 20}], "lSpellingErrors": []},
{"lGrammarErrors": [{"sLineId": "10836s", "sRuleId": "imp_va", "sType": "imp", "aSuggestions": ["Va"], "sMessage": "S’il s’agit d’un impératif, pas de “s”.", "URL": "", "nEndY": 57, "nEndX": 3, "nStartY": 57, "nStartX": 0}, {"sLineId": "10836s", "sRuleId": "imp_va", "sType": "imp", "aSuggestions": ["Va"], "sMessage": "S’il s’agit d’un impératif, pas de “s”.", "URL": "", "nEndY": 58, "nEndX": 3, "nStartY": 58, "nStartX": 0}], "lSpellingErrors": []}
]}