Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
SpellCheckEngine_Basic.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
6#include <cctype>
7#include <cmath>
8
16#include "Stroika/Foundation/Streams/TextToBinary.h"
17
18#include "SpellCheckEngine_Basic.h"
19
20using std::byte;
21
22using namespace Stroika::Foundation;
24
25using namespace Stroika::Frameworks;
26using namespace Stroika::Frameworks::Led;
27
29
30namespace {
31
32#if qStroika_Frameworks_Led_IncludeBakedInDictionaries
33#include "Dictionary-Compiled-US-English.inc"
34#endif
35
36 inline bool IsASCIIUpper (Led_tChar c)
37 {
38 return isascii (c) and isupper (c);
39 }
40
41 inline bool AsymmetricCaseInsensativeCompare (Led_tChar wordChar, Led_tChar dictChar)
42 {
43 if (wordChar == dictChar) {
44 return true;
45 }
46 if (isascii (wordChar) and isupper (wordChar)) {
47 return tolower (wordChar) == dictChar;
48 }
49 return false;
50 }
51 inline bool AsymmetricCaseInsensativeCompare (const Led_tChar* word, const Led_tChar* dictWord)
52 {
53 const Led_tChar* wi = word;
54 const Led_tChar* di = dictWord;
55 for (;; ++wi, ++di) {
56 if (not AsymmetricCaseInsensativeCompare (*wi, *di)) {
57 return false;
58 }
59 if (*wi == '\0' and *di == '\0') {
60 return true;
61 }
62 if (*wi == '\0' or *di == '\0') {
63 return false;
64 }
65 }
66 }
67
68 struct DictLookup_Compare {
69 DictLookup_Compare (const Led_tChar* base)
70 : fBase (base)
71 {
72 }
73 bool operator() (const SpellCheckEngine_Basic::InfoBlock& _Left, const Led_tString& _Right) const
74 {
75 // Speed tweeked compare... don't construct string object to safe time and possible heap fragmentation
76 bool answer = _Right.compare (0, _Right.length (), fBase + _Left.fIndex, _Left.fWordLen) > 0;
78 Led_tString left = Led_tString{fBase + _Left.fIndex, fBase + _Left.fIndex + _Left.fWordLen};
79 Assert (answer == (left < _Right));
80 }
81 return (answer);
82 }
83 const Led_tChar* fBase;
84 };
85}
86
87namespace {
88 inline bool IsASCIISpace (Led_tChar c)
89 {
90 return isascii (c) and isspace (c);
91 }
92 inline bool IsASCIIAlnum (Led_tChar c)
93 {
94 return isascii (c) and isalnum (c);
95 }
96}
97
98/*
99 ********************************************************************************
100 *************************** SpellCheckEngine_Basic *****************************
101 ********************************************************************************
102 */
103#if qStroika_Frameworks_Led_IncludeBakedInDictionaries
104const SpellCheckEngine_Basic::CompiledDictionary SpellCheckEngine_Basic::kDictionary_US_English (Dictionary_US_English);
105#endif
106
107SpellCheckEngine_Basic::SpellCheckEngine_Basic (const Dictionary* mainDictionary)
108 : inherited ()
109 , fDictionaries ()
110{
111 if (mainDictionary != NULL) {
112 fDictionaries.push_back (mainDictionary);
113 }
114}
115
116SpellCheckEngine_Basic::~SpellCheckEngine_Basic ()
117{
118}
119
120/*
121@METHOD: SpellCheckEngine_Basic::ScanForUndefinedWord
122@DESCRIPTION: <p>Overrides @'SpellCheckEngine::ScanForUndefinedWord'.</p>
123*/
124bool SpellCheckEngine_Basic::ScanForUndefinedWord (const Led_tChar* startBuf, const Led_tChar* endBuf, const Led_tChar** cursor,
125 const Led_tChar** wordStartResult, const Led_tChar** wordEndResult)
126{
127 RequireNotNull (startBuf);
128 RequireNotNull (endBuf);
129 RequireNotNull (cursor);
130 RequireNotNull (wordStartResult);
131 RequireNotNull (wordEndResult);
132 Require (*cursor == NULL or (*cursor >= startBuf and *cursor <= endBuf));
133
134 if (*cursor == NULL) {
135 *cursor = startBuf;
136 }
137
138 // preliminary implementation - shouldn't set output vars (wordStartResult/wordEndResult) result UNLESS WE are returning true...
139 while (ScanForWord (startBuf, endBuf, cursor, wordStartResult, wordEndResult)) {
140 if (not LookupWord (Led_tString{*wordStartResult, *wordEndResult}) and not OtherStringToIgnore (Led_tString{*wordStartResult, *wordEndResult})) {
141 return true;
142 }
143 }
144 return false;
145}
146
147/*
148@METHOD: SpellCheckEngine_Basic::LookupWord_
149@ACCESS: protected
150@DESCRIPTION: <p>Override (implement) @'SpellCheckEngine::LookupWord_'.</p>
151*/
152bool SpellCheckEngine_Basic::LookupWord_ (const Led_tString& checkWord, Led_tString* matchedWordResult)
153{
154 Invariant ();
155
156 /*
157 * See if we find the word as-is, and after that, try again after a few 'rewriting' tricks
158 */
159 if (LookupWordHelper_ (checkWord, matchedWordResult)) {
160 return true;
161 }
162
163 if (checkWord.empty ()) {
164 return false;
165 }
166
167 {
168 /*
169 * Don't compare completely case-insensatively. However- if we lookup a word which
170 * is capitalized, allow it to match a word in the dictionary whcih is not (since the given
171 * word could be starting a sentence).
172 */
173 if (checkWord[0] < 127 and isupper (checkWord[0])) {
174 Led_tString w2 = checkWord;
175 w2[0] = static_cast<char> (tolower (w2[0]));
176 if (LookupWordHelper_ (w2, matchedWordResult)) {
177 return true;
178 }
179
180 /*
181 * If the word is ALL UPPERCASE - then just treat it as a Capitalized word lookup. That is - "UPPER" should match
182 * the dictionary word "Upper" or "upper", but not "uPper".
183 */
184 {
185 bool allUpper = true;
186 Led_tString caseFixedWord = checkWord;
187 for (auto i = caseFixedWord.begin (); i != caseFixedWord.end (); ++i) {
188 if (IsASCIIUpper (*i)) {
189 *i = static_cast<char> (tolower (*i));
190 }
191 else {
192 allUpper = false;
193 break;
194 }
195 }
196 if (allUpper and LookupWordHelper_ (caseFixedWord, matchedWordResult)) {
197 return true;
198 }
199 caseFixedWord[0] = checkWord[0];
200 if (allUpper and LookupWordHelper_ (caseFixedWord, matchedWordResult)) {
201 return true;
202 }
203 }
204 }
205 }
206
207 // Look for hyphenated words
208 {
209 /*
210 * In our dictionary, we allow for hypenated words to be present. But thats not common. Its only for cases
211 * where the hyphenated word isn't constructable by other means (for example - a hypenated foreign expression
212 * where the elemental words are not part of the base language).
213 *
214 * Next, you can put hyphens between any normal english words (even with the usual rewriting rules - like
215 * appending 's' to the base noun word).
216 *
217 * Then - for some special cases - some words maybe entered in the dictionary with a suffixing hypen - to
218 * indicate that they ONLY match when used as a hypenated prefix. For example - 'anti-' might be so-coded,
219 * if you didn't want to allow for the word 'anti' by itself, but just as a prefix as in 'anti-war'.
220 */
221 Led_tString genMWR;
222 size_t lastFoundAt = 0;
223 bool mustCheckLastSeg = false;
224 for (size_t i = checkWord.find ('-'); i != Led_tString::npos or mustCheckLastSeg; i = checkWord.find ('-', i + 1)) {
225 if (i == Led_tString::npos) {
226 Assert (mustCheckLastSeg);
227 mustCheckLastSeg = false;
228 i = checkWord.length ();
229 }
230 else {
231 mustCheckLastSeg = true; // if we find a dash - we must check afer it as well
232 }
233 Led_tString segWord = Led_tString{checkWord.substr (lastFoundAt, i - lastFoundAt)};
234 Led_tString tmpMWR;
235 if (segWord.empty ()) {
236 // if any segment is empty - thats not legit - so treat that as misspelled
237 return false;
238 }
239 else if (LookupWord_ (segWord, &tmpMWR)) {
240 if (lastFoundAt != 0) {
241 genMWR += LED_TCHAR_OF ("-");
242 }
243 genMWR += tmpMWR;
244 }
245 else if (i != checkWord.length () and LookupWordHelper_ (segWord + LED_TCHAR_OF ("-"), &tmpMWR)) {
246 if (lastFoundAt != 0) {
247 genMWR += LED_TCHAR_OF ("-");
248 }
249 Assert (tmpMWR.length () >= 2); // must include the dash - whcih we strip...
250 genMWR += tmpMWR.substr (0, tmpMWR.length () - 1);
251 }
252 else {
253 // if any segment cannot be found - then the hyphenated word as a whole cannot
254 return false;
255 }
256 lastFoundAt = i + 1;
257 }
258
259 // If all segments found - then return the generated (combined genMWR) matchedWordResult.
260 // We know there were hyphens found if genMWR not empty
261 if (not genMWR.empty ()) {
262 if (matchedWordResult != NULL) {
263 *matchedWordResult = genMWR;
264 }
265 return true;
266 }
267 }
268
269 {
270 const wchar_t kRightSingleQuotationMark = L'\x2019';
271 size_t apos = checkWord.find (kRightSingleQuotationMark);
272 if (apos != Led_tString::npos) {
273 Led_tString tmp = checkWord;
274 tmp[apos] = '\'';
275 return LookupWord_ (tmp, matchedWordResult);
276 }
277 }
278
279 // PROBABALY LOTS MORE LOGIC/RULES TO ADD HERE!!!
280 {
281 }
282
283 return false;
284}
285
286/*
287@METHOD: SpellCheckEngine_Basic::LookupWordHelper_
288@ACCESS: private
289@DESCRIPTION: <p>Lookup the given word (case-sensative match - using @'DictLookup_Compare'). Looks through
290 all loaded dictionaries. Performs no fancy (linguistic) mapping (like tricks with punctutation
291 stripping etc). Thats handled at a higher level.</p>
292*/
293bool SpellCheckEngine_Basic::LookupWordHelper_ (const Led_tString& checkWord, Led_tString* matchedWordResult) const
294{
295 Invariant ();
296
297 for (auto i = fDictionaries.begin (); i != fDictionaries.end (); ++i) {
298 const Dictionary* dict = *i;
299 AssertNotNull (dict);
300
301 const InfoBlock* ibsStart = NULL;
302 const InfoBlock* ibsEnd = NULL;
303 dict->GetInfoBlocks (&ibsStart, &ibsEnd);
304 const Led_tChar* dictBufStart = dict->GetTextBase ();
305
306 const InfoBlock* r = lower_bound (ibsStart, ibsEnd, checkWord, DictLookup_Compare (dictBufStart));
307 if (r != ibsEnd) {
308 Led_tString x = Led_tString{dictBufStart + (*r).fIndex, dictBufStart + (*r).fIndex + (*r).fWordLen};
309 if (x == checkWord) {
310 if (matchedWordResult != NULL) {
311 *matchedWordResult = x;
312 }
313 return true;
314 }
315 }
316 }
317 return false;
318}
319
320/*
321@METHOD: SpellCheckEngine_Basic::OtherStringToIgnore
322@ACCESS: protected
323@DESCRIPTION: <p>Check if the given string should be ignored as an undefined word. Look for special patterns that won't be
324 found in our dictionary (e.g. numbers, strings of punctuation, etc).</p>
325 <p>This is typically called from @'SpellCheckEngine_Basic::ScanForUndefinedWord'</p>
326*/
327bool SpellCheckEngine_Basic::OtherStringToIgnore (const Led_tString& checkWord)
328{
329 return OtherStringToIgnore_AllPunctuation (checkWord) or OtherStringToIgnore_Sentinels (checkWord) or OtherStringToIgnore_Number (checkWord);
330}
331
332bool SpellCheckEngine_Basic::OtherStringToIgnore_AllPunctuation (const Led_tString& checkWord)
333{
334 for (size_t i = 0; i < checkWord.length (); ++i) {
335 Led_tChar c = checkWord[i];
336 if (not Character (c).IsPunctuation ()) {
337 return false;
338 }
339 }
340 return true;
341}
342
343bool SpellCheckEngine_Basic::OtherStringToIgnore_Number (const Led_tString& checkWord)
344{
345 for (size_t i = 0; i < checkWord.length (); ++i) {
346 Led_tChar c = checkWord[i];
347 // at least for English - we could tighten up the IsPunct call to just c==',' or c=='.' Not sure enough about
348 // other languages, so don't bother about this. Anyhow - stuff that is all punctuation and/or digits
349 // should probably be ignored anyhow... LGP 2003-06-25
350 if (not(Character (c).IsPunctuation () or Character (c).IsDigit ())) {
351 return false;
352 }
353 }
354 return true;
355}
356
357namespace {
358 // do this to avoid MSFT annoying _SCL_INSECURE_DEPRECATE warning I cannot seem to suppress
359 // with #praga (and don't want to use #define cuz makes it hard to work as a library)
360 // --LGP 2014-03-23
361 template <class BidirectionalIterator1, class BidirectionalIterator2>
362 BidirectionalIterator2 My_copy_backward_ (BidirectionalIterator1 first, BidirectionalIterator1 last, BidirectionalIterator2 result)
363 {
364 while (last != first)
365 *(--result) = *(--last);
366 return result;
367 }
368}
369
370namespace {
371 void AddToListsHelper (const size_t kMaxSug, Led_tString topSugs[], float topSugScores[], float* scoreCutOff, float s, const Led_tString& w)
372 {
373 RequireNotNull (scoreCutOff);
374 if (s > *scoreCutOff) {
375 // find where it fits in the array, and then adjust array
376 for (size_t ii = 0; ii < kMaxSug; ++ii) {
377 if (s > topSugScores[ii]) {
378 My_copy_backward_ (topSugs + ii, topSugs + kMaxSug - 1, topSugs + kMaxSug);
379 My_copy_backward_ (topSugScores + ii, topSugScores + kMaxSug - 1, topSugScores + kMaxSug);
380 topSugs[ii] = w;
381 topSugScores[ii] = s;
382 break;
383 }
384 }
385 *scoreCutOff = topSugScores[kMaxSug - 1];
386 }
387 }
388}
389
390vector<Led_tString> SpellCheckEngine_Basic::GenerateSuggestions (const Led_tString& misspelledWord)
391{
392 const size_t kMaxSug = 8;
393 Led_tString topSugs[kMaxSug];
394 float topSugScores[kMaxSug];
395 float scoreCutOff = -100000.0f;
396
397 fill (topSugScores, topSugScores + kMaxSug, scoreCutOff);
398
399 //See if the word is a hyphenated combination of two words
400 if (misspelledWord.find ('-') == Led_tString::npos) {
401 size_t maxInitSegSize = misspelledWord.size () - 1;
402 for (size_t initialWordSize = 1; initialWordSize < maxInitSegSize; ++initialWordSize) {
403 if (LookupWord (misspelledWord.substr (0, initialWordSize)) and LookupWord (misspelledWord.substr (initialWordSize))) {
404 AddToListsHelper (kMaxSug, topSugs, topSugScores, &scoreCutOff, -1.2f,
405 misspelledWord.substr (0, initialWordSize) + LED_TCHAR_OF ("-") + misspelledWord.substr (initialWordSize));
406 }
407 }
408 }
409
410 // Iterate over each dictionary
411 for (auto i = fDictionaries.begin (); i != fDictionaries.end (); ++i) {
412 const Dictionary* dict = *i;
413 AssertNotNull (dict);
414 const InfoBlock* ibsStart = NULL;
415 const InfoBlock* ibsEnd = NULL;
416 dict->GetInfoBlocks (&ibsStart, &ibsEnd);
417 const Led_tChar* dictBufStart = dict->GetTextBase ();
418 // Look at each word in the dictionary and assign each a score (see if they would be a good suggestion)
419 for (const InfoBlock* ib = ibsStart; ib != ibsEnd; ++ib) {
420 Led_tString w = Led_tString{dictBufStart + (*ib).fIndex, dictBufStart + (*ib).fIndex + (*ib).fWordLen};
421 float s = Heuristic (misspelledWord, w, scoreCutOff);
422 AddToListsHelper (kMaxSug, topSugs, topSugScores, &scoreCutOff, s, w);
423 }
424 }
425
426 // Look at the CASE of the original word - compate with the top suggestion. Perhaps
427 // clone the original suggestion and right a slightly better one which is the same as the first
428 // but with case properties improved.
429 if (not misspelledWord.empty ()) {
430 Led_tString topSug = topSugs[0];
431 if (not topSug.empty ()) {
432 bool capitalize = false;
433 bool allCaps = false;
434 {
435 if (Character (misspelledWord[0]).IsAlphabetic () and Character (topSug[0]).IsAlphabetic ()) {
436 if (isupper (misspelledWord[0]) and not isupper (topSug[0])) {
437 capitalize = true;
438 }
439 }
440 for (size_t i = 0; i < misspelledWord.length (); ++i) {
441 if (Character (misspelledWord[i]).IsAlphabetic ()) {
442 if (isupper (misspelledWord[i])) {
443 allCaps = true;
444 }
445 else {
446 allCaps = false;
447 break; // if we find ANY letters alpha - but non-capitalized - then breakout and say NOT whole thing capitalized
448 }
449 }
450 }
451 }
452 if (allCaps) {
453 Led_tString newWord = topSug;
454 {
455 for (size_t i = 0; i < newWord.length (); ++i) {
456 if (Character (newWord[i]).IsAlphabetic ()) {
457 newWord[i] = static_cast<char> (toupper (newWord[i]));
458 }
459 }
460 }
461 float newScore = topSugScores[0];
462 newScore += 0.1f;
463 AddToListsHelper (kMaxSug, topSugs, topSugScores, &scoreCutOff, newScore, newWord);
464 }
465 else if (capitalize) {
466 Led_tString newWord = topSug;
467 {
468 if (Character (newWord[0]).IsAlphabetic ()) {
469 newWord[0] = static_cast<char> (toupper (newWord[0]));
470 }
471 }
472 float newScore = topSugScores[0];
473 newScore += 0.1f;
474 AddToListsHelper (kMaxSug, topSugs, topSugScores, &scoreCutOff, newScore, newWord);
475 }
476 }
477 }
478
479 // Look for a big gap in scores, and perhaps cut-off there
480 size_t endScoreList = kMaxSug;
481 {
482 const float kTrigger = 1.5f; // MUST BE RETUNED EACH TIME I CHANGE HUERISTIC FUNCTION!
483 float lastScore = 0.0f;
484 for (size_t i = 0; i < kMaxSug; ++i) {
485 if (i != 0) {
486 if (fabs (lastScore - topSugScores[i]) > kTrigger) {
487 endScoreList = i;
488 break;
489 }
490 }
491 lastScore = topSugScores[i];
492 }
493 }
494
495 vector<Led_tString> result;
496 for (size_t j = 0; j < endScoreList; ++j) {
497 if (topSugs[j].empty ()) {
498 break;
499 }
500 else {
501 result.push_back (topSugs[j]);
502 }
503 }
504 return result;
505}
506
507SpellCheckEngine_Basic::UDInterface* SpellCheckEngine_Basic::GetUDInterface ()
508{
509 return NULL;
510}
511
512TextBreaks* SpellCheckEngine_Basic::PeekAtTextBreaksUsed ()
513{
514 return GetTextBreaker ().get ();
515}
516
517float SpellCheckEngine_Basic::Heuristic (const Led_tString& misspelledWord, const Led_tString& candidateWord, float atLeast)
518{
519 // Totally quick and dirty hack impl...
520 float h = 0.0f;
521 size_t mwl = misspelledWord.length ();
522 size_t cwl = candidateWord.length ();
523 float thisCharImportance = 2.0f;
524 for (size_t i = 0; i < mwl; ++i) {
525 if (i >= cwl) {
526 h -= thisCharImportance * 5.0f;
527 }
528 else if (misspelledWord[i] != candidateWord[i]) {
529 float prevH = h; // saved H so we can see how much we adjusted for transposition bonus
530
531 // do case mapping - REDO using CodePage.h. code(IsAlpha) etc... add a ToLower(C)
532 if (AsymmetricCaseInsensativeCompare (misspelledWord[i], candidateWord[i])) {
533 h -= thisCharImportance * 0.1f;
534 }
535 else {
536 // see if the right character is just before or after (a transpose? or missing character?)
537 if (i > 0 and AsymmetricCaseInsensativeCompare (misspelledWord[i], candidateWord[i - 1])) {
538 h -= thisCharImportance * 0.5f;
539 }
540 else if (i + 1 < cwl and AsymmetricCaseInsensativeCompare (misspelledWord[i], candidateWord[i + 1])) {
541 h -= thisCharImportance * 0.5f;
542 }
543 else {
544 h -= thisCharImportance * 3.0f;
545 }
546 }
547
548 // See if the mismatch is a transposition (with the following character- no need to check PREV because
549 // already captured in previous iteration)
550 if (i < mwl and (misspelledWord[i] == candidateWord[i + 1]) and (misspelledWord[i + 1] == candidateWord[i])) {
551 float thisLetterCost = h - prevH;
552 h += (-thisLetterCost) * 1.2f; // unsure what right cost - must take care of THIS char plus next neg...
553 }
554 }
555
556 // speed tweek - cut-off heuristic calc if we're already below the cut-off
557 if (h < atLeast) {
558 return -10000.0f;
559 }
560
561 // letters at the beginning are more important than at the end
562 if (i == 0) {
563 thisCharImportance *= 0.5f;
564 }
565 else if (i < 3) {
566 thisCharImportance *= 0.8f;
567 }
568 else {
569 thisCharImportance *= 0.9f;
570 }
571 }
572 {
573 const float kPenaltyForLettersOffEnd = 0.5f;
574 const float kPenaltyForLettersOffEndGrowthFactor = 1.2f;
575 float curPenalty = kPenaltyForLettersOffEnd;
576 for (size_t i = mwl; i < cwl; ++i) {
577 // extra chars off the end have growing significance...
578 h -= curPenalty;
579 curPenalty *= kPenaltyForLettersOffEndGrowthFactor;
580 }
581 }
582 return h;
583}
584
585/*
586@METHOD: SpellCheckEngine_Basic::ScanForWord
587@ACCESS: private
588@DESCRIPTION: <p>Look in the given buffer - starting at 'cursor' - for the next word Set wordStartResult/
589 wordEndResult according to what is found and return true if we find a word, and return false otherwise.
590 In either case - set 'cursor' on output to reflect how far we scanned ahead. It is indented that
591 this function be used iteratively and that you repeatedly pass IN the same cursor that was passed out.</p>
592*/
593bool SpellCheckEngine_Basic::ScanForWord (const Led_tChar* startBuf, const Led_tChar* endBuf, const Led_tChar** cursor,
594 const Led_tChar** wordStartResult, const Led_tChar** wordEndResult)
595{
596 RequireNotNull (startBuf);
597 RequireNotNull (endBuf);
598 RequireNotNull (cursor);
599 RequireNotNull (wordStartResult);
600 RequireNotNull (wordEndResult);
601 Require ((*cursor >= startBuf and *cursor <= endBuf));
602
603 if (*cursor >= endBuf) {
604 return false;
605 }
606
607 size_t bufLen = endBuf - startBuf;
608
609 size_t initialCrs = *cursor - startBuf;
610 size_t p = initialCrs;
611 size_t wordStart = 0;
612 size_t wordEnd = 0;
613 bool wordReal = false;
614 // Find a real word...
615 while (not wordReal or wordStart < initialCrs) {
616 GetTextBreaker ()->FindWordBreaks (startBuf, bufLen, p, &wordStart, &wordEnd, &wordReal);
617 if (not wordReal or wordStart < initialCrs) {
618 if (p < bufLen) {
619 p = Led_NextChar (&startBuf[p]) - startBuf;
620 wordStart = p;
621 wordEnd = p;
622 }
623 else {
624 wordStart = p;
625 wordEnd = p;
626 break;
627 }
628 }
629 }
630
631 if (not wordReal) {
632 // no real word to be found...
633 //maybe the first 'word' was a series of spaces and there may not have been a word after (end of buf).
634 *cursor = startBuf + wordEnd;
635 return false;
636 }
637
638 Assert (wordReal);
639 Assert (wordStart < wordEnd);
640 p = wordEnd;
641
642 Assert (*cursor <= startBuf + p);
643 Assert ((*cursor < startBuf + p) or (*cursor == endBuf));
644 *cursor = startBuf + p;
645
646 *wordStartResult = startBuf + wordStart;
647 *wordEndResult = startBuf + wordEnd;
648
649 return true;
650}
651
652vector<const SpellCheckEngine_Basic::Dictionary*> SpellCheckEngine_Basic::GetDictionaries () const
653{
654 return fDictionaries;
655}
656
657void SpellCheckEngine_Basic::SetDictionaries (const vector<const Dictionary*>& dictionaries)
658{
659 fDictionaries = dictionaries;
660}
661
662#if qStroika_Foundation_Debug_AssertionsChecked
663/*
664@METHOD: SpellCheckEngine_Basic::Invariant_
665@DESCRIPTION: <p>Check validity of SpellCheck engine. Called by @'SpellCheckEngine::Invariant'.</p>
666*/
667void SpellCheckEngine_Basic::Invariant_ () const
668{
669 Assert (sizeof (InfoBlock) == sizeof (int)); // Not a REAL requirement - but we want to make sure - for the most part - the
670 // compiler lays this out for us to be SMALL.
671 for (const Dictionary* dict : fDictionaries) {
672 AssertNotNull (dict);
673 const InfoBlock* ibsStart = NULL;
674 const InfoBlock* ibsEnd = NULL;
675 dict->GetInfoBlocks (&ibsStart, &ibsEnd);
676 const Led_tChar* dictBufStart = dict->GetTextBase ();
677 const Led_tChar* dictBufEnd = dict->GetTextEnd ();
678 if (dictBufStart != dictBufEnd) {
679 AssertNotNull (dictBufStart);
680 AssertNotNull (dictBufEnd);
681 Assert (dictBufStart <= dictBufEnd); // empty is a boring case - but I suppose not illegal
682 size_t bufSize = dictBufEnd - dictBufStart;
683 Led_tString prevWord;
684 for (const InfoBlock* i = ibsStart; i != ibsEnd; ++i) {
685 AssertNotNull (dictBufStart);
686 Assert ((*i).fIndex < bufSize);
687 Assert ((*i).fIndex + (*i).fWordLen <= bufSize);
688 Assert ((*i).fWordLen > 0);
689 Led_tString w = Led_tString{dictBufStart + (*i).fIndex, dictBufStart + (*i).fIndex + (*i).fWordLen};
690 Assert (not w.empty ());
691 if (i != ibsStart) {
692 // Assure words in alphabetical order
693 Assert (Led_tStrCmp (prevWord.c_str (), w.c_str ()) < 0);
694 }
695 prevWord = w;
696 }
697 }
698 }
699}
700#endif
701
702#if qStroika_Foundation_Debug_AssertionsChecked
703void SpellCheckEngine_Basic::RegressionTest ()
704{
705 try {
706 RegressionTest_1 ();
707 }
708 catch (...) {
709 Assert (false); // catch and just report here as assert errors and exceptions...
710 // (cuz currently on MacOS - for UNICODE builds - we generate
711 // exceptions cuz not enough UNICODE support there yet -- LGP 2003-05-30)
712 }
713}
714
715void SpellCheckEngine_Basic::RegressionTest_1 ()
716{
717 const Led_tChar* xxx = LED_TCHAR_OF ("IBM\na\napple\ndog\nfrog\ngood\nthis\nzipper\n");
718 SpellCheckEngine_Basic::EditableDictionary testerDict;
719 testerDict.ReadFromBuffer (xxx, xxx + Led_tStrlen (xxx));
720 SpellCheckEngine_Basic tester (&testerDict);
721 const Led_tChar* testText = LED_TCHAR_OF ("This is a very good test. ");
722
723 tester.Invariant ();
724
725 {
726 bool r1 = tester.LookupWord (LED_TCHAR_OF ("Frog"));
727 Assert (r1);
728 }
729
730 {
731 Led_tString r2S;
732 bool r2n = tester.LookupWord (LED_TCHAR_OF ("ziPPer"), &r2S);
733 bool r2y = tester.LookupWord (LED_TCHAR_OF ("Zipper"), &r2S);
734 Assert (not r2n and r2y and r2S == LED_TCHAR_OF ("zipper"));
735 }
736
737 {
738 Led_tString r3S;
739 Assert (not tester.LookupWord (LED_TCHAR_OF ("ibm")));
740 Assert (not tester.LookupWord (LED_TCHAR_OF ("Ibm")));
741 bool r3 = tester.LookupWord (LED_TCHAR_OF ("IBM"), &r3S);
742 Assert (r3 and r3S == LED_TCHAR_OF ("IBM"));
743 }
744
745 {
746 const Led_tChar* wordStart = NULL;
747 const Led_tChar* wordEnd = NULL;
748 const Led_tChar* p = NULL;
749 bool result = tester.ScanForUndefinedWord (testText, testText + Led_tStrlen (testText), &p, &wordStart, &wordEnd);
750 Assert (result and (Led_tString{wordStart, wordEnd} == LED_TCHAR_OF ("is")));
751 }
752
753 {
754 const Led_tChar* cursor = NULL;
755 const Led_tChar* wordStartResult = NULL;
756 const Led_tChar* wordEndResult = NULL;
757 int nWordsFound = 0;
758 while (tester.ScanForUndefinedWord (testText, testText + Led_tStrlen (testText), &cursor, &wordStartResult, &wordEndResult)) {
759 // we found a possible undefined word.
760 Led_tString word = Led_tString{wordStartResult, wordEndResult};
761 ++nWordsFound;
762 if (nWordsFound == 1) {
763 Assert ((Led_tString{wordStartResult, wordEndResult} == LED_TCHAR_OF ("is")));
764 }
765 if (nWordsFound == 2) {
766 Assert ((Led_tString{wordStartResult, wordEndResult} == LED_TCHAR_OF ("very")));
767 }
768 if (nWordsFound == 3) {
769 Assert ((Led_tString{wordStartResult, wordEndResult} == LED_TCHAR_OF ("test")));
770 }
771 }
772 Assert (nWordsFound == 3);
773 }
774}
775#endif
776
777/*
778 ********************************************************************************
779 ****************** SpellCheckEngine_Basic::EditableDictionary ******************
780 ********************************************************************************
781 */
782SpellCheckEngine_Basic::EditableDictionary::~EditableDictionary ()
783{
784 delete[] fDictBufStart;
785}
786
787void SpellCheckEngine_Basic::EditableDictionary::AddWordToUserDictionary (const Led_tString& word)
788{
789 fSortedWordList.insert (word);
790 ConstructInfoBlocksEtcFromWordList ();
791}
792
793const Led_tChar* SpellCheckEngine_Basic::EditableDictionary::GetTextBase () const
794{
795 return fDictBufStart;
796}
797
798const Led_tChar* SpellCheckEngine_Basic::EditableDictionary::GetTextEnd () const
799{
800 return fDictBufEnd;
801}
802
803void SpellCheckEngine_Basic::EditableDictionary::GetInfoBlocks (const InfoBlock** start, const InfoBlock** end) const
804{
805 RequireNotNull (start);
806 RequireNotNull (end);
807 *start = Containers::Start (fInfoBlocks);
808 *end = Containers::End (fInfoBlocks);
809}
810
811void SpellCheckEngine_Basic::EditableDictionary::ReadFromBuffer (const Led_tChar* readOnlyRAMDictStart, const Led_tChar* readOnlyRAMDictEnd)
812{
813 fSortedWordList.clear ();
814
815 // ASSUMES VALS COME IN CRLF or LF separated. This code does not assume that the
816 // input is already in dictionary order (though we write it that way).
817 for (const Led_tChar* p = readOnlyRAMDictStart; p < readOnlyRAMDictEnd;) {
818 // scan for \r or \n or \r\n to mark end of word
819 const Led_tChar* i1 = Led_tStrChr (p, '\r');
820 const Led_tChar* i2 = Led_tStrChr (p, '\n');
821 if (i1 == NULL) {
822 i1 = p + Led_tStrlen (p);
823 }
824 if (i2 == NULL) {
825 i2 = p + Led_tStrlen (p);
826 }
827 const Led_tChar* wordStart = p;
828 const Led_tChar* wordEnd = min (i1, i2);
829 if (wordStart != wordEnd) {
830 fSortedWordList.insert (Led_tString{wordStart, wordEnd});
831 }
832
833 p = wordEnd;
834 if (p < readOnlyRAMDictEnd) {
835 ++p; // skip CR or LF
836 }
837 // If CRLF then set p to point after CRLF (not just CR or LF)
838 if (p + 1 == i2) {
839 p = p + 1;
840 }
841 }
842 ConstructInfoBlocksEtcFromWordList ();
843}
844
845vector<Led_tChar> SpellCheckEngine_Basic::EditableDictionary::SaveToBuffer () const
846{
847 StackBuffer<Led_tChar> buf{1};
848
849#if qStroika_Platform_Windows
850 const Led_tChar kLineTerm[] = LED_TCHAR_OF ("\r\n");
851#else
852 const Led_tChar kLineTerm[] = LED_TCHAR_OF ("\n");
853#endif
854 const size_t kLineTerm_Length = std::size (kLineTerm) - 1;
855 size_t totalBufSizeSoFar = 0;
856 for (auto i = fSortedWordList.begin (); i != fSortedWordList.end (); ++i) {
857 {
858 size_t prevCopyTo = totalBufSizeSoFar;
859 totalBufSizeSoFar += (*i).length ();
860 buf.GrowToSize (totalBufSizeSoFar);
861 (void)::memcpy (static_cast<Led_tChar*> (buf) + prevCopyTo, Containers::Start (*i), i->size () * sizeof (Led_tChar));
862 }
863 {
864 size_t prevCopyTo = totalBufSizeSoFar;
865 totalBufSizeSoFar += kLineTerm_Length;
866 buf.GrowToSize (totalBufSizeSoFar);
867 (void)::memcpy (static_cast<Led_tChar*> (buf) + prevCopyTo, kLineTerm, kLineTerm_Length * sizeof (Led_tChar));
868 }
869 }
870 return vector<Led_tChar> (static_cast<Led_tChar*> (buf), static_cast<Led_tChar*> (buf) + totalBufSizeSoFar);
871}
872
873namespace {
874 // do this to avoid MSFT annoying _SCL_INSECURE_DEPRECATE warning I cannot seem to suppress
875 // with #praga (and don't want to use #define cuz makes it hard to work as a library)
876 // --LGP 2014-03-23
877 template <class InputIterator, class OutputIterator>
878 OutputIterator my_copy_ (InputIterator first, InputIterator last, OutputIterator result)
879 {
880 while (first != last) {
881 *result = *first;
882 ++result;
883 ++first;
884 }
885 return result;
886 }
887}
888
889void SpellCheckEngine_Basic::EditableDictionary::ConstructInfoBlocksEtcFromWordList ()
890{
891 // Clear old buffer values
892 delete[] fDictBufStart;
893 fDictBufStart = NULL;
894 fDictBufEnd = NULL;
895 fInfoBlocks.clear ();
896
897 size_t totalBlockSize = 0;
898 {
899 for (auto i = fSortedWordList.begin (); i != fSortedWordList.end (); ++i) {
900 totalBlockSize += (*i).length ();
901 }
902 }
903
904 fDictBufStart = new Led_tChar[totalBlockSize];
905 fDictBufEnd = fDictBufStart + totalBlockSize;
906
907 /*
908 * Now walk the list (already in the right order for the InfoBlock list), and fill in the fDictBuffer area,
909 * and the InfoBlock list at the same time.
910 */
911 Led_tChar* intoBufPtr = fDictBufStart;
912 for (auto i = fSortedWordList.begin (); i != fSortedWordList.end (); ++i) {
913 my_copy_ ((*i).begin (), (*i).end (), intoBufPtr);
914
915 InfoBlock iB;
916 (void)::memset (&iB, 0, sizeof (iB));
917 iB.fIndex = intoBufPtr - fDictBufStart;
918 iB.fWordLen = (*i).length ();
919 iB.fXXX = 0; //tmphack
920 Assert (iB.fWordLen > 0);
921 fInfoBlocks.push_back (iB);
922
923 intoBufPtr += (*i).length ();
924 }
925}
926
927/*
928 ********************************************************************************
929 ***************** SpellCheckEngine_Basic::CompiledDictionary *******************
930 ********************************************************************************
931 */
932SpellCheckEngine_Basic::CompiledDictionary::CompiledDictionary (const CompiledDictionaryData& data)
933 : fData{data}
934{
935}
936
937const Led_tChar* SpellCheckEngine_Basic::CompiledDictionary::GetTextBase () const
938{
939 return fData.fTextDataStart;
940}
941
942const Led_tChar* SpellCheckEngine_Basic::CompiledDictionary::GetTextEnd () const
943{
944 return fData.fTextDataEnd;
945}
946
947void SpellCheckEngine_Basic::CompiledDictionary::GetInfoBlocks (const InfoBlock** start, const InfoBlock** end) const
948{
949 RequireNotNull (start);
950 RequireNotNull (end);
951 *start = fData.fInfoBlocksStart;
952 *end = fData.fInfoBlocksEnd;
953}
954
955/*
956 ********************************************************************************
957 ***************************** TextBreaks_SpellChecker **************************
958 ********************************************************************************
959 */
960
961TextBreaks_SpellChecker::TextBreaks_SpellChecker ()
962{
963#if qStroika_Foundation_Debug_AssertionsChecked
964 // NB: since this is called in this CTOR - it doesn't capture (or pay attention to) subclass overrides of CharToCharacterClass
965 // That fact is important - since subclasses might change its result in a way to voilate this regression test. Thats fine - if its
966 // desired by the subclass. This is just a test to make sure logical changes we make to this code have intended
967 // consequences... LGP 2003-11-24
968 RegressionTest ();
969#endif
970}
971
972TextBreaks_SpellChecker::CharacterClasses TextBreaks_SpellChecker::CharToCharacterClass (const Led_tChar* startOfText, size_t lengthOfText,
973 const Led_tChar* charToExamine) const
974{
975 switch (*charToExamine) {
976 case '$':
977 case '%':
978 case '-': {
979 return (eWordClass);
980 } break;
981
982 case 0x2019: // curly apostrophe
983 case '\'': {
984 // APOSTROPHE between digits or letters
985 if (charToExamine > startOfText and charToExamine < &startOfText[lengthOfText]) {
986 const Led_tChar* prevChar = Led_PreviousChar (startOfText, charToExamine);
987 const Led_tChar* nextChar = charToExamine + 1; // cuz we KNOW we are single-byte...
988
989 // E.g.: Fred's or Lewis', but not Jim'
990 if ((IsASCIIAlnum (*prevChar) and *nextChar == 's') or (*prevChar == 's' and IsASCIISpace (*nextChar))) {
991 return (eWordClass);
992 }
993 }
994 } break;
995 }
996 return inherited::CharToCharacterClass (startOfText, lengthOfText, charToExamine);
997}
998
999#if qStroika_Foundation_Debug_AssertionsChecked
1000void TextBreaks_SpellChecker::RegressionTest ()
1001{
1002 {
1003 const Led_tChar* kTest = LED_TCHAR_OF ("This is a good test of Simone's bug with the 'word'.");
1004 size_t wordEndResult = 0;
1005 bool wordReal = 0;
1006
1007 FindLineBreaks (kTest, Led_tStrlen (kTest), 25, &wordEndResult, &wordReal);
1008 Assert (wordEndResult == 31);
1009 Assert (wordReal == true);
1010 }
1011}
1012#endif
1013
1014/*
1015 ********************************************************************************
1016 *********************** SpellCheckEngine_Basic_Simple **************************
1017 ********************************************************************************
1018 */
1019SpellCheckEngine_Basic_Simple::SpellCheckEngine_Basic_Simple ()
1020 : fMainDictionary{nullptr}
1021 , fUDName{}
1022 , fUD{nullptr}
1023{
1024#if qStroika_Frameworks_Led_IncludeBakedInDictionaries
1025 SetMainDictionary (&kDictionary_US_English);
1026#endif
1027}
1028
1029SpellCheckEngine_Basic_Simple::~SpellCheckEngine_Basic_Simple ()
1030{
1031 delete fUD;
1032}
1033
1034SpellCheckEngine_Basic_Simple::UDInterface* SpellCheckEngine_Basic_Simple::GetUDInterface ()
1035{
1036 return this;
1037}
1038
1039bool SpellCheckEngine_Basic_Simple::AddWordToUserDictionarySupported () const
1040{
1041 return fUD != NULL;
1042}
1043
1044void SpellCheckEngine_Basic_Simple::AddWordToUserDictionary (const Led_tString& word)
1045{
1046 Execution::ThrowIfNull (fUD);
1047 fUD->AddWordToUserDictionary (word);
1048 WriteToUD ();
1049}
1050
1051const SpellCheckEngine_Basic_Simple::Dictionary* SpellCheckEngine_Basic_Simple::GetMainDictionary () const
1052{
1053 return fMainDictionary;
1054}
1055
1056void SpellCheckEngine_Basic_Simple::SetMainDictionary (const Dictionary* mainDictionary)
1057{
1058 fMainDictionary = mainDictionary;
1059 vector<const Dictionary*> dicts;
1060 if (fMainDictionary != NULL) {
1061 dicts.push_back (fMainDictionary);
1062 }
1063 if (fUD != NULL) {
1064 dicts.push_back (fUD);
1065 }
1066 SetDictionaries (dicts);
1067}
1068
1069filesystem::path SpellCheckEngine_Basic_Simple::GetUserDictionary () const
1070{
1071 return fUDName;
1072}
1073
1074void SpellCheckEngine_Basic_Simple::SetUserDictionary (const filesystem::path& userDictionary)
1075{
1076 fUDName = userDictionary;
1077 bool noUD = userDictionary.empty ();
1078 delete fUD;
1079 fUD = NULL;
1080
1081 if (not noUD) {
1082 fUD = new EditableDictionary{};
1083 ReadFromUD ();
1084 }
1085 SetMainDictionary (fMainDictionary); // hack to force call to SetDictionaries ()
1086}
1087
1088void SpellCheckEngine_Basic_Simple::ReadFromUD ()
1089{
1090 /*
1091 * Ignore any errors reading from the UD (at least file-not-found errors).
1092 */
1093 try {
1094 Memory::BLOB b = IO::FileSystem::FileInputStream::New (filesystem::path (fUDName)).ReadAll ();
1095 span<const byte> rawByteSpan{b};
1096 CodeCvt<Led_tChar> converter{&rawByteSpan};
1097 size_t outCharCnt = converter.ComputeTargetCharacterBufferSize (rawByteSpan);
1098 Memory::StackBuffer<Led_tChar> fileData2{outCharCnt};
1099 auto charsRead = converter.Bytes2Characters (&rawByteSpan, span{fileData2});
1100 fUD->ReadFromBuffer (charsRead.data (), charsRead.data () + charsRead.size ());
1101 }
1102 catch (...) {
1103 }
1104}
1105
1106void SpellCheckEngine_Basic_Simple::WriteToUD ()
1107{
1108 Execution::ThrowIfNull (fUD);
1109 vector<Led_tChar> data = fUD->SaveToBuffer ();
1110 IO::FileSystem::FileOutputStream::Ptr writer = IO::FileSystem::FileOutputStream::New (filesystem::path (fUDName));
1111 Streams::TextToBinary::Writer::New (writer, UnicodeExternalEncodings::eUTF8, ByteOrderMark::eInclude).Write (span{data});
1112}
#define AssertNotNull(p)
Definition Assertions.h:336
#define qStroika_Foundation_Debug_AssertionsChecked
The qStroika_Foundation_Debug_AssertionsChecked flag determines if assertions are checked and validat...
Definition Assertions.h:49
#define RequireNotNull(p)
Definition Assertions.h:350
CodeCvt unifies byte <-> unicode conversions, vaguely inspired by (and wraps) std::codecvt,...
Definition CodeCvt.h:118
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...
nonvirtual void Write(span< ELEMENT_TYPE2, EXTENT_2 > elts) const
CONTAINER::value_type * End(CONTAINER &c)
For a contiguous container (such as a vector or basic_string) - find the pointer to the end of the co...
CONTAINER::value_type * Start(CONTAINER &c)
For a contiguous container (such as a vector or basic_string) - find the pointer to the start of the ...
Ptr New(const Streams::OutputStream::Ptr< byte > &src, const Characters::CodeCvt<> &char2OutputConverter)