KTextAddons

autocorrection.cpp
1/*
2 SPDX-FileCopyrightText: 2012-2024 Laurent Montel <montel@kde.org>
3 code based on calligra autocorrection.
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6*/
7
8#include "autocorrection.h"
9
10#include "autocorrectionutils.h"
11#include "textautocorrectionautocorrect_debug.h"
12#include <KColorScheme>
13#include <QLocale>
14#include <QStandardPaths>
15#include <QTextBlock>
16#include <QTextDocument>
17
18using namespace TextAutoCorrectionCore;
19using namespace Qt::Literals::StringLiterals;
20namespace TextAutoCorrectionCore
21{
22class AutoCorrectionPrivate
23{
24public:
25 AutoCorrectionPrivate()
26 : mAutoCorrectionSettings(new AutoCorrectionSettings)
27 {
28 const auto locale = QLocale::system();
29 mCacheNameOfDays.reserve(7);
30 for (int i = 1; i <= 7; ++i) {
31 mCacheNameOfDays.append(locale.dayName(i).toLower());
32 }
33 }
34 ~AutoCorrectionPrivate()
35 {
36 delete mAutoCorrectionSettings;
37 }
38 QString mWord;
39 QTextCursor mCursor;
40
41 QStringList mCacheNameOfDays;
42 QColor mLinkColor;
43 AutoCorrectionSettings *mAutoCorrectionSettings = nullptr;
44};
45}
46
47AutoCorrection::AutoCorrection()
48 : d(new AutoCorrectionPrivate)
49{
50}
51
52AutoCorrection::~AutoCorrection() = default;
53
54void AutoCorrection::selectStringOnMaximumSearchString(QTextCursor &cursor, int cursorPosition)
55{
56 cursor.setPosition(cursorPosition);
57
58 QTextBlock block = cursor.block();
59 int pos = qMax(block.position(), cursorPosition - d->mAutoCorrectionSettings->maxFindStringLength());
60
61 // TODO verify that pos == block.position() => it's a full line => not a piece of word
62 // TODO if not => check if pos -1 is a space => not a piece of word
63 // TODO otherwise move cursor until we detect a space
64 // TODO otherwise we must not autoconvert it.
65 if (pos != block.position()) {
66 const QString text = block.text();
67 const int currentPos = (pos - block.position());
68 if (!text.at(currentPos - 1).isSpace()) {
69 // qDebug() << " current Text " << text << " currentPos "<< currentPos << " pos " << pos;
70 // qDebug() << "selected text " << text.right(text.length() - currentPos);
71 // qDebug() << " text after " << text.at(currentPos - 1);
72 bool foundWord = false;
73 const int textLength(text.length());
74 for (int i = currentPos; i < textLength; ++i) {
75 if (text.at(i).isSpace()) {
76 pos = qMin(cursorPosition, pos + 1 + block.position());
77 foundWord = true;
78 break;
79 }
80 }
81 if (!foundWord) {
82 pos = cursorPosition;
83 }
84 }
85 }
86 cursor.setPosition(pos);
87 cursor.setPosition(cursorPosition, QTextCursor::KeepAnchor);
88}
89
90void AutoCorrection::selectPreviousWord(QTextCursor &cursor, int cursorPosition)
91{
92 cursor.setPosition(cursorPosition);
93 QTextBlock block = cursor.block();
94 cursor.setPosition(block.position());
95 cursorPosition -= block.position();
96 QString string = block.text();
97 int pos = 0;
98 bool space = false;
99 QString::Iterator iter = string.begin();
100 while (iter != string.end()) {
101 if (iter->isSpace()) {
102 if (space) {
103 // double spaces belong to the previous word
104 } else if (pos < cursorPosition) {
105 cursor.setPosition(pos + block.position() + 1); // +1 because we don't want to set it on the space itself
106 } else {
107 space = true;
108 }
109 } else if (space) {
110 break;
111 }
112 ++pos;
113 ++iter;
114 }
115 cursor.setPosition(pos + block.position(), QTextCursor::KeepAnchor);
116}
117
118bool AutoCorrection::autocorrect(bool htmlMode, QTextDocument &document, int &position)
119{
120 if (d->mAutoCorrectionSettings->isEnabledAutoCorrection()) {
121 d->mCursor = QTextCursor(&document);
122 d->mCursor.setPosition(position);
123
124 // If we already have a space not necessary to look at other autocorrect feature.
125 if (!singleSpaces()) {
126 return false;
127 }
128
129 int oldPosition = position;
130 selectPreviousWord(d->mCursor, position);
131 d->mWord = d->mCursor.selectedText();
132 if (d->mWord.isEmpty()) {
133 return true;
134 }
135 d->mCursor.beginEditBlock();
136 bool done = false;
137 if (htmlMode) {
138 done = autoFormatURLs();
139 if (!done) {
140 done = autoBoldUnderline();
141 // We replace */- by format => remove cursor position by 2
142 if (done) {
143 oldPosition -= 2;
144 }
145 }
146 if (!done) {
147 superscriptAppendix();
148 }
149 }
150 if (!done) {
151 done = autoFractions();
152 // We replace three characters with 1
153 if (done) {
154 oldPosition -= 2;
155 }
156 }
157 if (!done) {
158 uppercaseFirstCharOfSentence();
159 fixTwoUppercaseChars();
160 capitalizeWeekDays();
161 replaceTypographicQuotes();
162 if (d->mWord.length() <= 2) {
163 addNonBreakingSpace();
164 }
165 }
166
167 if (d->mCursor.selectedText() != d->mWord) {
168 d->mCursor.insertText(d->mWord);
169 }
170 position = oldPosition;
171 if (!done) {
172 selectStringOnMaximumSearchString(d->mCursor, position);
173 d->mWord = d->mCursor.selectedText();
174 if (!d->mWord.isEmpty()) {
175 const QStringList lst = AutoCorrectionUtils::wordsFromSentence(d->mWord);
176 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " lst " << lst;
177 for (const auto &string : lst) {
178 const int diffSize = d->mWord.length() - string.length();
179 d->mWord = string;
180 const int positionEnd(d->mCursor.selectionEnd());
181 d->mCursor.setPosition(d->mCursor.selectionStart() + diffSize);
182 d->mCursor.setPosition(positionEnd, QTextCursor::KeepAnchor);
183 const int newPos = advancedAutocorrect();
184 if (newPos != -1) {
185 if (d->mCursor.selectedText() != d->mWord) {
186 d->mCursor.insertText(d->mWord);
187 }
188 position = newPos;
189 break;
190 }
191 }
192 }
193 }
194 d->mCursor.endEditBlock();
195 }
196 return true;
197}
198
199void AutoCorrection::readConfig()
200{
201 d->mAutoCorrectionSettings->readConfig();
202}
203
204void AutoCorrection::writeConfig()
205{
206 d->mAutoCorrectionSettings->writeConfig();
207}
208
209void AutoCorrection::superscriptAppendix()
210{
211 if (!d->mAutoCorrectionSettings->isSuperScript()) {
212 return;
213 }
214
215 const QString trimmed = d->mWord.trimmed();
216 int startPos = -1;
217 int endPos = -1;
218 const int trimmedLenght(trimmed.length());
219
220 QHash<QString, QString>::const_iterator i = d->mAutoCorrectionSettings->superScriptEntries().constBegin();
221 while (i != d->mAutoCorrectionSettings->superScriptEntries().constEnd()) {
222 if (i.key() == trimmed) {
223 startPos = d->mCursor.selectionStart() + 1;
224 endPos = startPos - 1 + trimmedLenght;
225 break;
226 } else if (i.key() == "othernb"_L1) {
227 const int pos = trimmed.indexOf(i.value());
228 if (pos > 0) {
229 QString number = trimmed.left(pos);
231 bool found = true;
232 // don't apply superscript to 1th, 2th and 3th
233 const int numberLength(number.length());
234 if (numberLength == 1 && (*constIter == QLatin1Char('1') || *constIter == QLatin1Char('2') || *constIter == QLatin1Char('3'))) {
235 found = false;
236 }
237 if (found) {
238 while (constIter != number.constEnd()) {
239 if (!constIter->isNumber()) {
240 found = false;
241 break;
242 }
243 ++constIter;
244 }
245 }
246 if (found && numberLength + i.value().length() == trimmedLenght) {
247 startPos = d->mCursor.selectionStart() + pos;
248 endPos = startPos - pos + trimmedLenght;
249 break;
250 }
251 }
252 }
253 ++i;
254 }
255
256 if (startPos != -1 && endPos != -1) {
257 QTextCursor cursor(d->mCursor);
258 cursor.setPosition(startPos);
259 cursor.setPosition(endPos, QTextCursor::KeepAnchor);
260
261 QTextCharFormat format;
263 cursor.mergeCharFormat(format);
264 }
265}
266
267void AutoCorrection::addNonBreakingSpace()
268{
269 if (d->mAutoCorrectionSettings->isAddNonBreakingSpace() && d->mAutoCorrectionSettings->isFrenchLanguage()) {
270 const QTextBlock block = d->mCursor.block();
271 const QString text = block.text();
272 const QChar lastChar = text.at(d->mCursor.position() - 1 - block.position());
273
274 if (lastChar == QLatin1Char(':') || lastChar == QLatin1Char(';') || lastChar == QLatin1Char('!') || lastChar == QLatin1Char('?')
275 || lastChar == QLatin1Char('%')) {
276 const int pos = d->mCursor.position() - 2 - block.position();
277 if (pos >= 0) {
278 const QChar previousChar = text.at(pos);
279 if (previousChar.isSpace()) {
280 QTextCursor cursor(d->mCursor);
281 cursor.setPosition(pos);
282 cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
283 cursor.deleteChar();
284 d->mCursor.insertText(d->mAutoCorrectionSettings->nonBreakingSpace());
285 }
286 }
287 } else {
288 // °C (degrees)
289 const int pos = d->mCursor.position() - 2 - block.position();
290 if (pos >= 0) {
291 const QChar previousChar = text.at(pos);
292
293 if (lastChar == QLatin1Char('C') && previousChar == QChar(0x000B0)) {
294 const int pos = d->mCursor.position() - 3 - block.position();
295 if (pos >= 0) {
296 const QChar previousCharFromDegrees = text.at(pos);
297 if (previousCharFromDegrees.isSpace()) {
298 QTextCursor cursor(d->mCursor);
299 cursor.setPosition(pos);
300 cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
301 cursor.deleteChar();
302 d->mCursor.insertText(d->mAutoCorrectionSettings->nonBreakingSpace());
303 }
304 }
305 }
306 }
307 }
308 }
309}
310
311bool AutoCorrection::autoBoldUnderline()
312{
313 if (!d->mAutoCorrectionSettings->isAutoBoldUnderline()) {
314 return false;
315 }
316 const QString trimmed = d->mWord.trimmed();
317
318 const auto trimmedLength{trimmed.length()};
319 if (trimmedLength < 3) {
320 return false;
321 }
322
323 const QChar trimmedFirstChar(trimmed.at(0));
324 const QChar trimmedLastChar(trimmed.at(trimmedLength - 1));
325 const bool underline = (trimmedFirstChar == QLatin1Char('_') && trimmedLastChar == QLatin1Char('_'));
326 const bool bold = (trimmedFirstChar == QLatin1Char('*') && trimmedLastChar == QLatin1Char('*'));
327 const bool strikeOut = (trimmedFirstChar == QLatin1Char('-') && trimmedLastChar == QLatin1Char('-'));
328 if (underline || bold || strikeOut) {
329 const int startPos = d->mCursor.selectionStart();
330 const QString replacement = trimmed.mid(1, trimmedLength - 2);
331 bool foundLetterNumber = false;
332
333 QString::ConstIterator constIter = replacement.constBegin();
334 while (constIter != replacement.constEnd()) {
335 if (constIter->isLetterOrNumber()) {
336 foundLetterNumber = true;
337 break;
338 }
339 ++constIter;
340 }
341
342 // if no letter/number found, don't apply autocorrection like in OOo 2.x
343 if (!foundLetterNumber) {
344 return false;
345 }
346 d->mCursor.setPosition(startPos);
347 d->mCursor.setPosition(startPos + trimmedLength, QTextCursor::KeepAnchor);
348 d->mCursor.insertText(replacement);
349 d->mCursor.setPosition(startPos);
350 d->mCursor.setPosition(startPos + replacement.length(), QTextCursor::KeepAnchor);
351
352 QTextCharFormat format;
353 format.setFontUnderline(underline ? true : d->mCursor.charFormat().fontUnderline());
354 format.setFontWeight(bold ? QFont::Bold : d->mCursor.charFormat().fontWeight());
355 format.setFontStrikeOut(strikeOut ? true : d->mCursor.charFormat().fontStrikeOut());
356 d->mCursor.mergeCharFormat(format);
357
358 // to avoid the selection being replaced by d->mWord
359 d->mWord = d->mCursor.selectedText();
360 return true;
361 } else {
362 return false;
363 }
364
365 return true;
366}
367
368QColor AutoCorrection::linkColor()
369{
370 if (!d->mLinkColor.isValid()) {
372 }
373 return d->mLinkColor;
374}
375
376AutoCorrectionSettings *AutoCorrection::autoCorrectionSettings() const
377{
378 return d->mAutoCorrectionSettings;
379}
380
381void AutoCorrection::setAutoCorrectionSettings(AutoCorrectionSettings *newAutoCorrectionSettings)
382{
383 if (d->mAutoCorrectionSettings != newAutoCorrectionSettings) {
384 delete d->mAutoCorrectionSettings;
385 }
386 d->mAutoCorrectionSettings = newAutoCorrectionSettings;
387}
388
389bool AutoCorrection::autoFormatURLs()
390{
391 if (!d->mAutoCorrectionSettings->isAutoFormatUrl()) {
392 return false;
393 }
394
395 const QString link = autoDetectURL(d->mWord);
396 if (link.isNull()) {
397 return false;
398 }
399
400 const QString trimmed = d->mWord.trimmed();
401 const int startPos = d->mCursor.selectionStart();
402 d->mCursor.setPosition(startPos);
403 d->mCursor.setPosition(startPos + trimmed.length(), QTextCursor::KeepAnchor);
404
405 QTextCharFormat format;
406 format.setAnchorHref(link);
407 format.setFontItalic(true);
408 format.setAnchor(true);
410 format.setUnderlineColor(linkColor());
411 format.setForeground(linkColor());
412 d->mCursor.mergeCharFormat(format);
413
414 d->mWord = d->mCursor.selectedText();
415 return true;
416}
417
418QString AutoCorrection::autoDetectURL(const QString &_word) const
419{
420 QString word = _word;
421 /* this method is ported from lib/kotext/KoAutoFormat.cpp KoAutoFormat::doAutoDetectUrl
422 * from Calligra 1.x branch */
423 // qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << "link:" << word;
424
425 // we start by iterating through a list of schemes, and if no match is found,
426 // we proceed to 3 special cases
427
428 // list of the schemes, starting with http:// as most probable
429 const QStringList schemes = QStringList() << QStringLiteral("http://") << QStringLiteral("https://") << QStringLiteral("mailto:/")
430 << QStringLiteral("ftp://") << QStringLiteral("file://") << QStringLiteral("git://") << QStringLiteral("sftp://")
431 << QStringLiteral("magnet:?") << QStringLiteral("smb://") << QStringLiteral("nfs://") << QStringLiteral("fish://")
432 << QStringLiteral("ssh://") << QStringLiteral("telnet://") << QStringLiteral("irc://") << QStringLiteral("sip:")
433 << QStringLiteral("news:") << QStringLiteral("gopher://") << QStringLiteral("nntp://") << QStringLiteral("geo:")
434 << QStringLiteral("udp://") << QStringLiteral("rsync://") << QStringLiteral("dns://");
435
436 enum LinkType {
437 UNCLASSIFIED,
438 SCHEME,
439 MAILTO,
440 WWW,
441 FTP,
442 };
443 LinkType linkType = UNCLASSIFIED;
444 int pos = 0;
445 int contentPos = 0;
446
447 // TODO: ideally there would be proper pattern matching,
448 // instead of just searching for some key string, like done with indexOf.
449 // This should reduce the amount of possible mismatches
450 for (const QString &scheme : schemes) {
451 pos = word.indexOf(scheme);
452 if (pos != -1) {
453 linkType = SCHEME;
454 contentPos = pos + scheme.length();
455 break; // break as soon as you get a match
456 }
457 }
458
459 if (linkType == UNCLASSIFIED) {
460 pos = word.indexOf("www."_L1, 0, Qt::CaseInsensitive);
461 if (pos != -1 && word.indexOf(QLatin1Char('.'), pos + 4) != -1) {
462 linkType = WWW;
463 contentPos = pos + 4;
464 }
465 }
466 if (linkType == UNCLASSIFIED) {
467 pos = word.indexOf("ftp."_L1, 0, Qt::CaseInsensitive);
468 if (pos != -1 && word.indexOf(QLatin1Char('.'), pos + 4) != -1) {
469 linkType = FTP;
470 contentPos = pos + 4;
471 }
472 }
473 if (linkType == UNCLASSIFIED) {
474 const int separatorPos = word.lastIndexOf(QLatin1Char('@'));
475 if (separatorPos != -1) {
476 pos = separatorPos - 1;
477 QChar c;
478 while (pos >= 0) {
479 c = word.at(pos);
480 if ((c.isPunct() && c != QLatin1Char('.') && c != QLatin1Char('_')) || (c == QLatin1Char('@'))) {
481 pos = -2;
482 break;
483 } else {
484 --pos;
485 }
486 }
487 if (pos == -1) { // a valid address
488 ++pos;
489 contentPos = separatorPos + 1;
490 linkType = MAILTO;
491 }
492 }
493 }
494
495 if (linkType != UNCLASSIFIED) {
496 // A URL inside e.g. quotes (like "http://www.calligra.org" with the quotes)
497 // shouldn't include the quote in the URL.
498 int lastPos = word.length() - 1;
499 while (!word.at(lastPos).isLetter() && !word.at(lastPos).isDigit() && word.at(lastPos) != QLatin1Char('/')) {
500 --lastPos;
501 }
502 // sanity check: was there no real content behind the key string?
503 if (lastPos < contentPos) {
504 return QString();
505 }
506 word.truncate(lastPos + 1);
507 word.remove(0, pos);
508 switch (linkType) {
509 case MAILTO:
510 word.prepend("mailto:"_L1);
511 break;
512 case WWW:
513 word.prepend("http://"_L1);
514 break;
515 case FTP:
516 word.prepend("ftps://"_L1);
517 break;
518 case SCHEME:
519 case UNCLASSIFIED:
520 break;
521 }
522 return word;
523 }
524 return {};
525}
526
527void AutoCorrection::fixTwoUppercaseChars()
528{
529 if (!d->mAutoCorrectionSettings->isFixTwoUppercaseChars()) {
530 return;
531 }
532 if (d->mWord.length() <= 2) {
533 return;
534 }
535
536 if (d->mAutoCorrectionSettings->twoUpperLetterExceptions().contains(d->mWord.trimmed())) {
537 return;
538 }
539
540 const QChar firstChar = d->mWord.at(0);
541 const QChar secondChar = d->mWord.at(1);
542
543 if (secondChar.isUpper() && firstChar.isUpper()) {
544 const QChar thirdChar = d->mWord.at(2);
545
546 if (thirdChar.isLower()) {
547 d->mWord.replace(1, 1, secondChar.toLower());
548 }
549 }
550}
551
552// Return true if we can add space
553bool AutoCorrection::singleSpaces() const
554{
555 if (!d->mAutoCorrectionSettings->isSingleSpaces()) {
556 return true;
557 }
558 if (!d->mCursor.atBlockStart()) {
559 // then when the prev char is also a space, don't insert one.
560 const QTextBlock block = d->mCursor.block();
561 const QString text = block.text();
562 if (text.at(d->mCursor.position() - 1 - block.position()) == QLatin1Char(' ')) {
563 return false;
564 }
565 }
566 return true;
567}
568
569void AutoCorrection::capitalizeWeekDays()
570{
571 if (!d->mAutoCorrectionSettings->isCapitalizeWeekDays()) {
572 return;
573 }
574
575 const QString trimmed = d->mWord.trimmed();
576 for (const QString &name : std::as_const(d->mCacheNameOfDays)) {
577 if (trimmed == name) {
578 const int pos = d->mWord.indexOf(name);
579 d->mWord.replace(pos, 1, name.at(0).toUpper());
580 return;
581 }
582 }
583}
584
585bool AutoCorrection::excludeToUppercase(const QString &word) const
586{
587 if (word.startsWith(QLatin1StringView("http://")) || word.startsWith("www."_L1) || word.startsWith("mailto:"_L1)
588 || word.startsWith(QLatin1StringView("ftp://")) || word.startsWith("https://"_L1) || word.startsWith("ftps://"_L1)) {
589 return true;
590 }
591 return false;
592}
593
594void AutoCorrection::uppercaseFirstCharOfSentence()
595{
596 if (!d->mAutoCorrectionSettings->isUppercaseFirstCharOfSentence()) {
597 return;
598 }
599
600 int startPos = d->mCursor.selectionStart();
601 QTextBlock block = d->mCursor.block();
602
603 d->mCursor.setPosition(block.position());
604 d->mCursor.setPosition(startPos, QTextCursor::KeepAnchor);
605
606 int position = d->mCursor.selectionEnd();
607
608 const QString text = d->mCursor.selectedText();
609
610 if (text.isEmpty()) { // start of a paragraph
611 if (!excludeToUppercase(d->mWord)) {
612 d->mWord.replace(0, 1, d->mWord.at(0).toUpper());
613 }
614 } else {
615 QString::ConstIterator constIter = text.constEnd();
616 --constIter;
617
618 while (constIter != text.constBegin()) {
619 while (constIter != text.begin() && constIter->isSpace()) {
620 --constIter;
621 --position;
622 }
623
624 if (constIter != text.constBegin() && (*constIter == QLatin1Char('.') || *constIter == QLatin1Char('!') || *constIter == QLatin1Char('?'))) {
625 constIter--;
626 while (constIter != text.constBegin() && !(constIter->isLetter())) {
627 --position;
628 --constIter;
629 }
630 selectPreviousWord(d->mCursor, --position);
631 const QString prevWord = d->mCursor.selectedText();
632
633 // search for exception
634 if (d->mAutoCorrectionSettings->upperCaseExceptions().contains(prevWord.trimmed())) {
635 break;
636 }
637 if (excludeToUppercase(d->mWord)) {
638 break;
639 }
640
641 d->mWord.replace(0, 1, d->mWord.at(0).toUpper());
642 break;
643 } else {
644 break;
645 }
646 }
647 }
648
649 d->mCursor.setPosition(startPos);
650 d->mCursor.setPosition(startPos + d->mWord.length(), QTextCursor::KeepAnchor);
651}
652
653bool AutoCorrection::autoFractions() const
654{
655 if (!d->mAutoCorrectionSettings->isAutoFractions()) {
656 return false;
657 }
658
659 const QString trimmed = d->mWord.trimmed();
660 const auto trimmedLength{trimmed.length()};
661 if (trimmedLength > 3) {
662 const QChar x = trimmed.at(3);
663 const uchar xunicode = x.unicode();
664 if (!(xunicode == '.' || xunicode == ',' || xunicode == '?' || xunicode == '!' || xunicode == ':' || xunicode == ';')) {
665 return false;
666 }
667 } else if (trimmedLength < 3) {
668 return false;
669 }
670
671 if (trimmed.startsWith("1/2"_L1)) {
672 d->mWord.replace(0, 3, QStringLiteral("½"));
673 } else if (trimmed.startsWith("1/4"_L1)) {
674 d->mWord.replace(0, 3, QStringLiteral("¼"));
675 } else if (trimmed.startsWith("3/4"_L1)) {
676 d->mWord.replace(0, 3, QStringLiteral("¾"));
677 } else {
678 return false;
679 }
680
681 return true;
682}
683
684int AutoCorrection::advancedAutocorrect()
685{
686 if (!d->mAutoCorrectionSettings->isAdvancedAutocorrect()) {
687 return -1;
688 }
689 if (d->mAutoCorrectionSettings->autocorrectEntries().isEmpty()) {
690 return -1;
691 }
692 const QString trimmedWord = d->mWord.trimmed();
693 if (trimmedWord.isEmpty()) {
694 return -1;
695 }
696 QString actualWord = trimmedWord;
697
698 const int actualWordLength(actualWord.length());
699 if (actualWordLength < d->mAutoCorrectionSettings->minFindStringLength()) {
700 return -1;
701 }
702 if (actualWordLength > d->mAutoCorrectionSettings->maxFindStringLength()) {
703 return -1;
704 }
705 const int startPos = d->mCursor.selectionStart();
706 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << "d->mCursor " << d->mCursor.selectedText() << " startPos " << startPos;
707 const int length = d->mWord.length();
708 // If the last char is punctuation, drop it for now
709 bool hasPunctuation = false;
710 const QChar lastChar = actualWord.at(actualWord.length() - 1);
711 const ushort charUnicode = lastChar.unicode();
712 if (charUnicode == '.' || charUnicode == ',' || charUnicode == '?' || charUnicode == '!' || charUnicode == ';') {
713 hasPunctuation = true;
714 actualWord.chop(1);
715 } else if (charUnicode == ':' && actualWord.at(0).unicode() != ':') {
716 hasPunctuation = true;
717 actualWord.chop(1);
718 }
719 QString actualWordWithFirstUpperCase = actualWord;
720 if (!actualWordWithFirstUpperCase.isEmpty()) {
721 actualWordWithFirstUpperCase[0] = actualWordWithFirstUpperCase[0].toUpper();
722 }
723 QHashIterator<QString, QString> i(d->mAutoCorrectionSettings->autocorrectEntries());
724 while (i.hasNext()) {
725 i.next();
726 const auto key = i.key();
727 const auto keyLength{key.length()};
728 if (hasPunctuation) {
729 // We remove 1 element when we have punctuation
730 if (keyLength != actualWordLength - 1) {
731 continue;
732 }
733 } else {
734 if (keyLength != actualWordLength) {
735 continue;
736 }
737 }
738 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " i.key() " << key << "actual" << actualWord;
739 if (actualWord.endsWith(key) || actualWord.endsWith(key, Qt::CaseInsensitive) || actualWordWithFirstUpperCase.endsWith(key)) {
740 int pos = d->mWord.lastIndexOf(key);
741 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " pos 1 " << pos << " d->mWord " << d->mWord;
742 if (pos == -1) {
743 pos = actualWord.toLower().lastIndexOf(key);
744 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " pos 2 " << pos;
745 if (pos == -1) {
746 pos = actualWordWithFirstUpperCase.lastIndexOf(key);
747 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " pos 3 " << pos;
748 if (pos == -1) {
749 continue;
750 }
751 }
752 }
753 QString replacement = i.value();
754
755 // qDebug() << " actualWord " << actualWord << " pos " << pos << " actualWord.size" << actualWord.length() << "actualWordWithFirstUpperCase "
756 // <<actualWordWithFirstUpperCase; qDebug() << " d->mWord " << d->mWord << " i.key() " << i.key() << "replacement " << replacement; Keep capitalized
757 // words capitalized. (Necessary to make sure the first letters match???)
758 const QChar actualWordFirstChar = d->mWord.at(pos);
759 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " actualWordFirstChar " << actualWordFirstChar;
760
761 const QChar replacementFirstChar = replacement[0];
762 if (actualWordFirstChar.isUpper() && replacementFirstChar.isLower()) {
763 replacement[0] = replacementFirstChar.toUpper();
764 } else if (actualWordFirstChar.isLower() && replacementFirstChar.isUpper()) {
765 replacement[0] = replacementFirstChar.toLower();
766 }
767
768 // If a punctuation mark was on the end originally, add it back on
769 if (hasPunctuation) {
770 replacement.append(lastChar);
771 }
772
773 d->mWord.replace(pos, pos + trimmedWord.length(), replacement);
774
775 // We do replacement here, since the length of new word might be different from length of
776 // the old world. Length difference might affect other type of autocorrection
777 d->mCursor.setPosition(startPos);
778 d->mCursor.setPosition(startPos + length, QTextCursor::KeepAnchor);
779 d->mCursor.insertText(d->mWord);
780 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " insert text " << d->mWord << " startPos " << startPos;
781 d->mCursor.setPosition(startPos); // also restore the selection
782 const int newPosition = startPos + d->mWord.length();
783 d->mCursor.setPosition(newPosition, QTextCursor::KeepAnchor);
784 return newPosition;
785 }
786 }
787 return -1;
788}
789
790void AutoCorrection::replaceTypographicQuotes()
791{
792 // TODO add french quotes support.
793 /* this method is ported from lib/kotext/KoAutoFormat.cpp KoAutoFormat::doTypographicQuotes
794 * from Calligra 1.x branch */
795
796 if (!(d->mAutoCorrectionSettings->isReplaceDoubleQuotes() && d->mWord.contains(QLatin1Char('"')))
797 && !(d->mAutoCorrectionSettings->isReplaceSingleQuotes() && d->mWord.contains(QLatin1Char('\'')))) {
798 return;
799 }
800
801 const bool addNonBreakingSpace = (d->mAutoCorrectionSettings->isFrenchLanguage() && d->mAutoCorrectionSettings->isAddNonBreakingSpace());
802
803 // Need to determine if we want a starting or ending quote.
804 // we use a starting quote in three cases:
805 // 1. if the previous character is a space
806 // 2. if the previous character is some kind of opening punctuation (e.g., "(", "[", or "{")
807 // a. and the character before that is not an opening quote (so that we get quotations of single characters
808 // right)
809 // 3. if the previous character is an opening quote (so that we get nested quotations right)
810 // a. and the character before that is not an opening quote (so that we get quotations of single characters
811 // right)
812 // b. and the previous quote of a different kind (so that we get empty quotations right)
813
814 bool ending = true;
815 for (int i = d->mWord.length(); i > 1; --i) {
816 const QChar c = d->mWord.at(i - 1);
817 if (c == QLatin1Char('"') || c == QLatin1Char('\'')) {
818 const bool doubleQuotes = (c == QLatin1Char('"'));
819 if (i > 2) {
820 QChar::Category c1 = d->mWord.at(i - 1).category();
821
822 // case 1 and 2
824 || c1 == QChar::Other_Control) {
825 ending = false;
826 }
827
828 // case 3
830 QChar openingQuote;
831
832 if (doubleQuotes) {
833 if (d->mAutoCorrectionSettings->isReplaceDoubleQuotesByFrenchQuotes()) {
834 openingQuote = d->mAutoCorrectionSettings->doubleFrenchQuotes().begin;
835 } else {
836 openingQuote = d->mAutoCorrectionSettings->typographicDoubleQuotes().begin;
837 }
838 } else {
839 openingQuote = d->mAutoCorrectionSettings->typographicSingleQuotes().begin;
840 }
841
842 // case 3b
843 if (d->mWord.at(i - 1) != openingQuote) {
844 ending = false;
845 }
846 }
847 }
848 // case 2a and 3a
849 if (i > 3 && !ending) {
850 const QChar::Category c2 = (d->mWord.at(i - 2)).category();
851 ending = (c2 == QChar::Punctuation_InitialQuote);
852 }
853
854 if (doubleQuotes && d->mAutoCorrectionSettings->isReplaceDoubleQuotes()) {
855 if (ending) {
856 const QChar endQuote = d->mAutoCorrectionSettings->isReplaceDoubleQuotesByFrenchQuotes()
857 ? d->mAutoCorrectionSettings->doubleFrenchQuotes().end
858 : d->mAutoCorrectionSettings->typographicDoubleQuotes().end;
859 if (addNonBreakingSpace) {
860 d->mWord.replace(i - 1, 2, QString(d->mAutoCorrectionSettings->nonBreakingSpace() + endQuote));
861 } else {
862 d->mWord[i - 1] = endQuote;
863 }
864 } else {
865 const QChar beginQuote = d->mAutoCorrectionSettings->isReplaceDoubleQuotesByFrenchQuotes()
866 ? d->mAutoCorrectionSettings->doubleFrenchQuotes().begin
867 : d->mAutoCorrectionSettings->typographicDoubleQuotes().begin;
868 if (addNonBreakingSpace) {
869 d->mWord.replace(i - 1, 2, QString(d->mAutoCorrectionSettings->nonBreakingSpace() + beginQuote));
870 } else {
871 d->mWord[i - 1] = beginQuote;
872 }
873 }
874 } else if (d->mAutoCorrectionSettings->isReplaceSingleQuotes()) {
875 if (ending) {
876 if (addNonBreakingSpace) {
877 d->mWord.replace(i - 1,
878 2,
879 QString(d->mAutoCorrectionSettings->nonBreakingSpace() + d->mAutoCorrectionSettings->typographicSingleQuotes().end));
880 } else {
881 d->mWord[i - 1] = d->mAutoCorrectionSettings->typographicSingleQuotes().end;
882 }
883 } else {
884 if (addNonBreakingSpace) {
885 d->mWord.replace(i - 1,
886 2,
887 QString(d->mAutoCorrectionSettings->nonBreakingSpace() + d->mAutoCorrectionSettings->typographicSingleQuotes().begin));
888 } else {
889 d->mWord[i - 1] = d->mAutoCorrectionSettings->typographicSingleQuotes().begin;
890 }
891 }
892 }
893 }
894 }
895
896 // first character
897 if (d->mWord.at(0) == QLatin1Char('"') && d->mAutoCorrectionSettings->isReplaceDoubleQuotes()) {
898 const QChar beginQuote = d->mAutoCorrectionSettings->isReplaceDoubleQuotesByFrenchQuotes()
899 ? d->mAutoCorrectionSettings->doubleFrenchQuotes().begin
900 : d->mAutoCorrectionSettings->typographicDoubleQuotes().begin;
901 d->mWord[0] = beginQuote;
902 if (addNonBreakingSpace) {
903 d->mWord.insert(1, d->mAutoCorrectionSettings->nonBreakingSpace());
904 }
905 } else if (d->mWord.at(0) == QLatin1Char('\'') && d->mAutoCorrectionSettings->isReplaceSingleQuotes()) {
906 d->mWord[0] = d->mAutoCorrectionSettings->typographicSingleQuotes().begin;
907 if (addNonBreakingSpace) {
908 d->mWord.insert(1, d->mAutoCorrectionSettings->nonBreakingSpace());
909 }
910 }
911}
912
913void AutoCorrection::loadGlobalFileName(const QString &fname)
914{
915 d->mAutoCorrectionSettings->loadGlobalFileName(fname);
916}
917
918void AutoCorrection::writeAutoCorrectionXmlFile(const QString &filename)
919{
920 d->mAutoCorrectionSettings->writeAutoCorrectionFile(filename);
921}
QBrush foreground(ForegroundRole=NormalText) const
KIOCORE_EXPORT QString number(KIO::filesize_t size)
KIOCORE_EXPORT CopyJob * link(const QList< QUrl > &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
QString name(StandardAction id)
const QList< QKeySequence > & end()
const QColor & color() const const
bool isDigit(char32_t ucs4)
bool isLetter(char32_t ucs4)
bool isLower(char32_t ucs4)
bool isPunct(char32_t ucs4)
bool isSpace(char32_t ucs4)
bool isUpper(char32_t ucs4)
char32_t toLower(char32_t ucs4)
char32_t toUpper(char32_t ucs4)
char16_t & unicode()
void append(QList< T > &&value)
void reserve(qsizetype size)
QLocale system()
QString & append(QChar ch)
const QChar at(qsizetype position) const const
iterator begin()
void chop(qsizetype n)
const_iterator constBegin() const const
const_iterator constEnd() const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString & prepend(QChar ch)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toUpper() const const
QString trimmed() const const
void truncate(qsizetype position)
CaseInsensitive
int position() const const
QString text() const const
void setAnchor(bool anchor)
void setAnchorHref(const QString &value)
void setFontItalic(bool italic)
void setFontStrikeOut(bool strikeOut)
void setFontUnderline(bool underline)
void setFontWeight(int weight)
void setUnderlineColor(const QColor &color)
void setUnderlineStyle(UnderlineStyle style)
void setVerticalAlignment(VerticalAlignment alignment)
QTextBlock block() const const
void deleteChar()
void mergeCharFormat(const QTextCharFormat &modifier)
void setPosition(int pos, MoveMode m)
void setForeground(const QBrush &brush)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:56:02 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.