File indexing completed on 2024-12-22 04:28:08

0001 /*
0002   SPDX-FileCopyrightText: 2012-2024 Laurent Montel <montel@kde.org>
0003   code based on calligra autocorrection.
0004 
0005   SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "autocorrection.h"
0009 
0010 #include "autocorrectionutils.h"
0011 #include "textautocorrectionautocorrect_debug.h"
0012 #include <KColorScheme>
0013 #include <QLocale>
0014 #include <QStandardPaths>
0015 #include <QTextBlock>
0016 #include <QTextDocument>
0017 
0018 using namespace TextAutoCorrectionCore;
0019 
0020 namespace TextAutoCorrectionCore
0021 {
0022 class AutoCorrectionPrivate
0023 {
0024 public:
0025     AutoCorrectionPrivate()
0026         : mAutoCorrectionSettings(new AutoCorrectionSettings)
0027     {
0028         const auto locale = QLocale::system();
0029         mCacheNameOfDays.reserve(7);
0030         for (int i = 1; i <= 7; ++i) {
0031             mCacheNameOfDays.append(locale.dayName(i).toLower());
0032         }
0033     }
0034     ~AutoCorrectionPrivate()
0035     {
0036         delete mAutoCorrectionSettings;
0037     }
0038     QString mWord;
0039     QTextCursor mCursor;
0040 
0041     QStringList mCacheNameOfDays;
0042     QColor mLinkColor;
0043     AutoCorrectionSettings *mAutoCorrectionSettings = nullptr;
0044 };
0045 }
0046 
0047 AutoCorrection::AutoCorrection()
0048     : d(new AutoCorrectionPrivate())
0049 {
0050 }
0051 
0052 AutoCorrection::~AutoCorrection() = default;
0053 
0054 void AutoCorrection::selectStringOnMaximumSearchString(QTextCursor &cursor, int cursorPosition)
0055 {
0056     cursor.setPosition(cursorPosition);
0057 
0058     QTextBlock block = cursor.block();
0059     int pos = qMax(block.position(), cursorPosition - d->mAutoCorrectionSettings->maxFindStringLength());
0060 
0061     // TODO verify that pos == block.position() => it's a full line => not a piece of word
0062     // TODO if not => check if pos -1 is a space => not a piece of word
0063     // TODO otherwise move cursor until we detect a space
0064     // TODO otherwise we must not autoconvert it.
0065     if (pos != block.position()) {
0066         const QString text = block.text();
0067         const int currentPos = (pos - block.position());
0068         if (!text.at(currentPos - 1).isSpace()) {
0069             // qDebug() << " current Text " << text << " currentPos "<< currentPos << " pos " << pos;
0070             // qDebug() << "selected text " << text.right(text.length() - currentPos);
0071             // qDebug() << "  text after " << text.at(currentPos - 1);
0072             bool foundWord = false;
0073             const int textLength(text.length());
0074             for (int i = currentPos; i < textLength; ++i) {
0075                 if (text.at(i).isSpace()) {
0076                     pos = qMin(cursorPosition, pos + 1 + block.position());
0077                     foundWord = true;
0078                     break;
0079                 }
0080             }
0081             if (!foundWord) {
0082                 pos = cursorPosition;
0083             }
0084         }
0085     }
0086     cursor.setPosition(pos);
0087     cursor.setPosition(cursorPosition, QTextCursor::KeepAnchor);
0088 }
0089 
0090 void AutoCorrection::selectPreviousWord(QTextCursor &cursor, int cursorPosition)
0091 {
0092     cursor.setPosition(cursorPosition);
0093     QTextBlock block = cursor.block();
0094     cursor.setPosition(block.position());
0095     cursorPosition -= block.position();
0096     QString string = block.text();
0097     int pos = 0;
0098     bool space = false;
0099     QString::Iterator iter = string.begin();
0100     while (iter != string.end()) {
0101         if (iter->isSpace()) {
0102             if (space) {
0103                 // double spaces belong to the previous word
0104             } else if (pos < cursorPosition) {
0105                 cursor.setPosition(pos + block.position() + 1); // +1 because we don't want to set it on the space itself
0106             } else {
0107                 space = true;
0108             }
0109         } else if (space) {
0110             break;
0111         }
0112         ++pos;
0113         ++iter;
0114     }
0115     cursor.setPosition(pos + block.position(), QTextCursor::KeepAnchor);
0116 }
0117 
0118 bool AutoCorrection::autocorrect(bool htmlMode, QTextDocument &document, int &position)
0119 {
0120     if (d->mAutoCorrectionSettings->isEnabledAutoCorrection()) {
0121         d->mCursor = QTextCursor(&document);
0122         d->mCursor.setPosition(position);
0123 
0124         // If we already have a space not necessary to look at other autocorrect feature.
0125         if (!singleSpaces()) {
0126             return false;
0127         }
0128 
0129         int oldPosition = position;
0130         selectPreviousWord(d->mCursor, position);
0131         d->mWord = d->mCursor.selectedText();
0132         if (d->mWord.isEmpty()) {
0133             return true;
0134         }
0135         d->mCursor.beginEditBlock();
0136         bool done = false;
0137         if (htmlMode) {
0138             done = autoFormatURLs();
0139             if (!done) {
0140                 done = autoBoldUnderline();
0141                 // We replace */- by format => remove cursor position by 2
0142                 if (done) {
0143                     oldPosition -= 2;
0144                 }
0145             }
0146             if (!done) {
0147                 superscriptAppendix();
0148             }
0149         }
0150         if (!done) {
0151             done = autoFractions();
0152             // We replace three characters with 1
0153             if (done) {
0154                 oldPosition -= 2;
0155             }
0156         }
0157         if (!done) {
0158             uppercaseFirstCharOfSentence();
0159             fixTwoUppercaseChars();
0160             capitalizeWeekDays();
0161             replaceTypographicQuotes();
0162             if (d->mWord.length() <= 2) {
0163                 addNonBreakingSpace();
0164             }
0165         }
0166 
0167         if (d->mCursor.selectedText() != d->mWord) {
0168             d->mCursor.insertText(d->mWord);
0169         }
0170         position = oldPosition;
0171         if (!done) {
0172             selectStringOnMaximumSearchString(d->mCursor, position);
0173             d->mWord = d->mCursor.selectedText();
0174             if (!d->mWord.isEmpty()) {
0175                 const QStringList lst = AutoCorrectionUtils::wordsFromSentence(d->mWord);
0176                 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " lst " << lst;
0177                 for (const auto &string : lst) {
0178                     const int diffSize = d->mWord.length() - string.length();
0179                     d->mWord = string;
0180                     const int positionEnd(d->mCursor.selectionEnd());
0181                     d->mCursor.setPosition(d->mCursor.selectionStart() + diffSize);
0182                     d->mCursor.setPosition(positionEnd, QTextCursor::KeepAnchor);
0183                     const int newPos = advancedAutocorrect();
0184                     if (newPos != -1) {
0185                         if (d->mCursor.selectedText() != d->mWord) {
0186                             d->mCursor.insertText(d->mWord);
0187                         }
0188                         position = newPos;
0189                         break;
0190                     }
0191                 }
0192             }
0193         }
0194         d->mCursor.endEditBlock();
0195     }
0196     return true;
0197 }
0198 
0199 void AutoCorrection::readConfig()
0200 {
0201     d->mAutoCorrectionSettings->readConfig();
0202 }
0203 
0204 void AutoCorrection::writeConfig()
0205 {
0206     d->mAutoCorrectionSettings->writeConfig();
0207 }
0208 
0209 void AutoCorrection::superscriptAppendix()
0210 {
0211     if (!d->mAutoCorrectionSettings->isSuperScript()) {
0212         return;
0213     }
0214 
0215     const QString trimmed = d->mWord.trimmed();
0216     int startPos = -1;
0217     int endPos = -1;
0218     const int trimmedLenght(trimmed.length());
0219 
0220     QHash<QString, QString>::const_iterator i = d->mAutoCorrectionSettings->superScriptEntries().constBegin();
0221     while (i != d->mAutoCorrectionSettings->superScriptEntries().constEnd()) {
0222         if (i.key() == trimmed) {
0223             startPos = d->mCursor.selectionStart() + 1;
0224             endPos = startPos - 1 + trimmedLenght;
0225             break;
0226         } else if (i.key() == QLatin1String("othernb")) {
0227             const int pos = trimmed.indexOf(i.value());
0228             if (pos > 0) {
0229                 QString number = trimmed.left(pos);
0230                 QString::ConstIterator constIter = number.constBegin();
0231                 bool found = true;
0232                 // don't apply superscript to 1th, 2th and 3th
0233                 const int numberLength(number.length());
0234                 if (numberLength == 1 && (*constIter == QLatin1Char('1') || *constIter == QLatin1Char('2') || *constIter == QLatin1Char('3'))) {
0235                     found = false;
0236                 }
0237                 if (found) {
0238                     while (constIter != number.constEnd()) {
0239                         if (!constIter->isNumber()) {
0240                             found = false;
0241                             break;
0242                         }
0243                         ++constIter;
0244                     }
0245                 }
0246                 if (found && numberLength + i.value().length() == trimmedLenght) {
0247                     startPos = d->mCursor.selectionStart() + pos;
0248                     endPos = startPos - pos + trimmedLenght;
0249                     break;
0250                 }
0251             }
0252         }
0253         ++i;
0254     }
0255 
0256     if (startPos != -1 && endPos != -1) {
0257         QTextCursor cursor(d->mCursor);
0258         cursor.setPosition(startPos);
0259         cursor.setPosition(endPos, QTextCursor::KeepAnchor);
0260 
0261         QTextCharFormat format;
0262         format.setVerticalAlignment(QTextCharFormat::AlignSuperScript);
0263         cursor.mergeCharFormat(format);
0264     }
0265 }
0266 
0267 void AutoCorrection::addNonBreakingSpace()
0268 {
0269     if (d->mAutoCorrectionSettings->isAddNonBreakingSpace() && d->mAutoCorrectionSettings->isFrenchLanguage()) {
0270         const QTextBlock block = d->mCursor.block();
0271         const QString text = block.text();
0272         const QChar lastChar = text.at(d->mCursor.position() - 1 - block.position());
0273 
0274         if (lastChar == QLatin1Char(':') || lastChar == QLatin1Char(';') || lastChar == QLatin1Char('!') || lastChar == QLatin1Char('?')
0275             || lastChar == QLatin1Char('%')) {
0276             const int pos = d->mCursor.position() - 2 - block.position();
0277             if (pos >= 0) {
0278                 const QChar previousChar = text.at(pos);
0279                 if (previousChar.isSpace()) {
0280                     QTextCursor cursor(d->mCursor);
0281                     cursor.setPosition(pos);
0282                     cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
0283                     cursor.deleteChar();
0284                     d->mCursor.insertText(d->mAutoCorrectionSettings->nonBreakingSpace());
0285                 }
0286             }
0287         } else {
0288             // °C (degrees)
0289             const int pos = d->mCursor.position() - 2 - block.position();
0290             if (pos >= 0) {
0291                 const QChar previousChar = text.at(pos);
0292 
0293                 if (lastChar == QLatin1Char('C') && previousChar == QChar(0x000B0)) {
0294                     const int pos = d->mCursor.position() - 3 - block.position();
0295                     if (pos >= 0) {
0296                         const QChar previousCharFromDegrees = text.at(pos);
0297                         if (previousCharFromDegrees.isSpace()) {
0298                             QTextCursor cursor(d->mCursor);
0299                             cursor.setPosition(pos);
0300                             cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
0301                             cursor.deleteChar();
0302                             d->mCursor.insertText(d->mAutoCorrectionSettings->nonBreakingSpace());
0303                         }
0304                     }
0305                 }
0306             }
0307         }
0308     }
0309 }
0310 
0311 bool AutoCorrection::autoBoldUnderline()
0312 {
0313     if (!d->mAutoCorrectionSettings->isAutoBoldUnderline()) {
0314         return false;
0315     }
0316     const QString trimmed = d->mWord.trimmed();
0317 
0318     const auto trimmedLength{trimmed.length()};
0319     if (trimmedLength < 3) {
0320         return false;
0321     }
0322 
0323     const QChar trimmedFirstChar(trimmed.at(0));
0324     const QChar trimmedLastChar(trimmed.at(trimmedLength - 1));
0325     const bool underline = (trimmedFirstChar == QLatin1Char('_') && trimmedLastChar == QLatin1Char('_'));
0326     const bool bold = (trimmedFirstChar == QLatin1Char('*') && trimmedLastChar == QLatin1Char('*'));
0327     const bool strikeOut = (trimmedFirstChar == QLatin1Char('-') && trimmedLastChar == QLatin1Char('-'));
0328     if (underline || bold || strikeOut) {
0329         const int startPos = d->mCursor.selectionStart();
0330         const QString replacement = trimmed.mid(1, trimmedLength - 2);
0331         bool foundLetterNumber = false;
0332 
0333         QString::ConstIterator constIter = replacement.constBegin();
0334         while (constIter != replacement.constEnd()) {
0335             if (constIter->isLetterOrNumber()) {
0336                 foundLetterNumber = true;
0337                 break;
0338             }
0339             ++constIter;
0340         }
0341 
0342         // if no letter/number found, don't apply autocorrection like in OOo 2.x
0343         if (!foundLetterNumber) {
0344             return false;
0345         }
0346         d->mCursor.setPosition(startPos);
0347         d->mCursor.setPosition(startPos + trimmedLength, QTextCursor::KeepAnchor);
0348         d->mCursor.insertText(replacement);
0349         d->mCursor.setPosition(startPos);
0350         d->mCursor.setPosition(startPos + replacement.length(), QTextCursor::KeepAnchor);
0351 
0352         QTextCharFormat format;
0353         format.setFontUnderline(underline ? true : d->mCursor.charFormat().fontUnderline());
0354         format.setFontWeight(bold ? QFont::Bold : d->mCursor.charFormat().fontWeight());
0355         format.setFontStrikeOut(strikeOut ? true : d->mCursor.charFormat().fontStrikeOut());
0356         d->mCursor.mergeCharFormat(format);
0357 
0358         // to avoid the selection being replaced by d->mWord
0359         d->mWord = d->mCursor.selectedText();
0360         return true;
0361     } else {
0362         return false;
0363     }
0364 
0365     return true;
0366 }
0367 
0368 QColor AutoCorrection::linkColor()
0369 {
0370     if (!d->mLinkColor.isValid()) {
0371         d->mLinkColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color();
0372     }
0373     return d->mLinkColor;
0374 }
0375 
0376 AutoCorrectionSettings *AutoCorrection::autoCorrectionSettings() const
0377 {
0378     return d->mAutoCorrectionSettings;
0379 }
0380 
0381 void AutoCorrection::setAutoCorrectionSettings(AutoCorrectionSettings *newAutoCorrectionSettings)
0382 {
0383     if (d->mAutoCorrectionSettings != newAutoCorrectionSettings) {
0384         delete d->mAutoCorrectionSettings;
0385     }
0386     d->mAutoCorrectionSettings = newAutoCorrectionSettings;
0387 }
0388 
0389 bool AutoCorrection::autoFormatURLs()
0390 {
0391     if (!d->mAutoCorrectionSettings->isAutoFormatUrl()) {
0392         return false;
0393     }
0394 
0395     const QString link = autoDetectURL(d->mWord);
0396     if (link.isNull()) {
0397         return false;
0398     }
0399 
0400     const QString trimmed = d->mWord.trimmed();
0401     const int startPos = d->mCursor.selectionStart();
0402     d->mCursor.setPosition(startPos);
0403     d->mCursor.setPosition(startPos + trimmed.length(), QTextCursor::KeepAnchor);
0404 
0405     QTextCharFormat format;
0406     format.setAnchorHref(link);
0407     format.setFontItalic(true);
0408     format.setAnchor(true);
0409     format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
0410     format.setUnderlineColor(linkColor());
0411     format.setForeground(linkColor());
0412     d->mCursor.mergeCharFormat(format);
0413 
0414     d->mWord = d->mCursor.selectedText();
0415     return true;
0416 }
0417 
0418 QString AutoCorrection::autoDetectURL(const QString &_word) const
0419 {
0420     QString word = _word;
0421     /* this method is ported from lib/kotext/KoAutoFormat.cpp KoAutoFormat::doAutoDetectUrl
0422      * from Calligra 1.x branch */
0423     // qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << "link:" << word;
0424 
0425     // we start by iterating through a list of schemes, and if no match is found,
0426     // we proceed to 3 special cases
0427 
0428     // list of the schemes, starting with http:// as most probable
0429     const QStringList schemes = QStringList() << QStringLiteral("http://") << QStringLiteral("https://") << QStringLiteral("mailto:/")
0430                                               << QStringLiteral("ftp://") << QStringLiteral("file://") << QStringLiteral("git://") << QStringLiteral("sftp://")
0431                                               << QStringLiteral("magnet:?") << QStringLiteral("smb://") << QStringLiteral("nfs://") << QStringLiteral("fish://")
0432                                               << QStringLiteral("ssh://") << QStringLiteral("telnet://") << QStringLiteral("irc://") << QStringLiteral("sip:")
0433                                               << QStringLiteral("news:") << QStringLiteral("gopher://") << QStringLiteral("nntp://") << QStringLiteral("geo:")
0434                                               << QStringLiteral("udp://") << QStringLiteral("rsync://") << QStringLiteral("dns://");
0435 
0436     enum LinkType {
0437         UNCLASSIFIED,
0438         SCHEME,
0439         MAILTO,
0440         WWW,
0441         FTP,
0442     };
0443     LinkType linkType = UNCLASSIFIED;
0444     int pos = 0;
0445     int contentPos = 0;
0446 
0447     // TODO: ideally there would be proper pattern matching,
0448     // instead of just searching for some key string, like done with indexOf.
0449     // This should reduce the amount of possible mismatches
0450     for (const QString &scheme : schemes) {
0451         pos = word.indexOf(scheme);
0452         if (pos != -1) {
0453             linkType = SCHEME;
0454             contentPos = pos + scheme.length();
0455             break; // break as soon as you get a match
0456         }
0457     }
0458 
0459     if (linkType == UNCLASSIFIED) {
0460         pos = word.indexOf(QLatin1String("www."), 0, Qt::CaseInsensitive);
0461         if (pos != -1 && word.indexOf(QLatin1Char('.'), pos + 4) != -1) {
0462             linkType = WWW;
0463             contentPos = pos + 4;
0464         }
0465     }
0466     if (linkType == UNCLASSIFIED) {
0467         pos = word.indexOf(QLatin1String("ftp."), 0, Qt::CaseInsensitive);
0468         if (pos != -1 && word.indexOf(QLatin1Char('.'), pos + 4) != -1) {
0469             linkType = FTP;
0470             contentPos = pos + 4;
0471         }
0472     }
0473     if (linkType == UNCLASSIFIED) {
0474         const int separatorPos = word.lastIndexOf(QLatin1Char('@'));
0475         if (separatorPos != -1) {
0476             pos = separatorPos - 1;
0477             QChar c;
0478             while (pos >= 0) {
0479                 c = word.at(pos);
0480                 if ((c.isPunct() && c != QLatin1Char('.') && c != QLatin1Char('_')) || (c == QLatin1Char('@'))) {
0481                     pos = -2;
0482                     break;
0483                 } else {
0484                     --pos;
0485                 }
0486             }
0487             if (pos == -1) { // a valid address
0488                 ++pos;
0489                 contentPos = separatorPos + 1;
0490                 linkType = MAILTO;
0491             }
0492         }
0493     }
0494 
0495     if (linkType != UNCLASSIFIED) {
0496         // A URL inside e.g. quotes (like "http://www.calligra.org" with the quotes)
0497         // shouldn't include the quote in the URL.
0498         int lastPos = word.length() - 1;
0499         while (!word.at(lastPos).isLetter() && !word.at(lastPos).isDigit() && word.at(lastPos) != QLatin1Char('/')) {
0500             --lastPos;
0501         }
0502         // sanity check: was there no real content behind the key string?
0503         if (lastPos < contentPos) {
0504             return QString();
0505         }
0506         word.truncate(lastPos + 1);
0507         word.remove(0, pos);
0508         switch (linkType) {
0509         case MAILTO:
0510             word.prepend(QLatin1String("mailto:"));
0511             break;
0512         case WWW:
0513             word.prepend(QLatin1String("http://"));
0514             break;
0515         case FTP:
0516             word.prepend(QLatin1String("ftps://"));
0517             break;
0518         case SCHEME:
0519         case UNCLASSIFIED:
0520             break;
0521         }
0522         return word;
0523     }
0524     return {};
0525 }
0526 
0527 void AutoCorrection::fixTwoUppercaseChars()
0528 {
0529     if (!d->mAutoCorrectionSettings->isFixTwoUppercaseChars()) {
0530         return;
0531     }
0532     if (d->mWord.length() <= 2) {
0533         return;
0534     }
0535 
0536     if (d->mAutoCorrectionSettings->twoUpperLetterExceptions().contains(d->mWord.trimmed())) {
0537         return;
0538     }
0539 
0540     const QChar firstChar = d->mWord.at(0);
0541     const QChar secondChar = d->mWord.at(1);
0542 
0543     if (secondChar.isUpper() && firstChar.isUpper()) {
0544         const QChar thirdChar = d->mWord.at(2);
0545 
0546         if (thirdChar.isLower()) {
0547             d->mWord.replace(1, 1, secondChar.toLower());
0548         }
0549     }
0550 }
0551 
0552 // Return true if we can add space
0553 bool AutoCorrection::singleSpaces() const
0554 {
0555     if (!d->mAutoCorrectionSettings->isSingleSpaces()) {
0556         return true;
0557     }
0558     if (!d->mCursor.atBlockStart()) {
0559         // then when the prev char is also a space, don't insert one.
0560         const QTextBlock block = d->mCursor.block();
0561         const QString text = block.text();
0562         if (text.at(d->mCursor.position() - 1 - block.position()) == QLatin1Char(' ')) {
0563             return false;
0564         }
0565     }
0566     return true;
0567 }
0568 
0569 void AutoCorrection::capitalizeWeekDays()
0570 {
0571     if (!d->mAutoCorrectionSettings->isCapitalizeWeekDays()) {
0572         return;
0573     }
0574 
0575     const QString trimmed = d->mWord.trimmed();
0576     for (const QString &name : std::as_const(d->mCacheNameOfDays)) {
0577         if (trimmed == name) {
0578             const int pos = d->mWord.indexOf(name);
0579             d->mWord.replace(pos, 1, name.at(0).toUpper());
0580             return;
0581         }
0582     }
0583 }
0584 
0585 bool AutoCorrection::excludeToUppercase(const QString &word) const
0586 {
0587     if (word.startsWith(QLatin1String("http://")) || word.startsWith(QLatin1String("www.")) || word.startsWith(QLatin1String("mailto:"))
0588         || word.startsWith(QLatin1String("ftp://")) || word.startsWith(QLatin1String("https://")) || word.startsWith(QLatin1String("ftps://"))) {
0589         return true;
0590     }
0591     return false;
0592 }
0593 
0594 void AutoCorrection::uppercaseFirstCharOfSentence()
0595 {
0596     if (!d->mAutoCorrectionSettings->isUppercaseFirstCharOfSentence()) {
0597         return;
0598     }
0599 
0600     int startPos = d->mCursor.selectionStart();
0601     QTextBlock block = d->mCursor.block();
0602 
0603     d->mCursor.setPosition(block.position());
0604     d->mCursor.setPosition(startPos, QTextCursor::KeepAnchor);
0605 
0606     int position = d->mCursor.selectionEnd();
0607 
0608     const QString text = d->mCursor.selectedText();
0609 
0610     if (text.isEmpty()) { // start of a paragraph
0611         if (!excludeToUppercase(d->mWord)) {
0612             d->mWord.replace(0, 1, d->mWord.at(0).toUpper());
0613         }
0614     } else {
0615         QString::ConstIterator constIter = text.constEnd();
0616         --constIter;
0617 
0618         while (constIter != text.constBegin()) {
0619             while (constIter != text.begin() && constIter->isSpace()) {
0620                 --constIter;
0621                 --position;
0622             }
0623 
0624             if (constIter != text.constBegin() && (*constIter == QLatin1Char('.') || *constIter == QLatin1Char('!') || *constIter == QLatin1Char('?'))) {
0625                 constIter--;
0626                 while (constIter != text.constBegin() && !(constIter->isLetter())) {
0627                     --position;
0628                     --constIter;
0629                 }
0630                 selectPreviousWord(d->mCursor, --position);
0631                 const QString prevWord = d->mCursor.selectedText();
0632 
0633                 // search for exception
0634                 if (d->mAutoCorrectionSettings->upperCaseExceptions().contains(prevWord.trimmed())) {
0635                     break;
0636                 }
0637                 if (excludeToUppercase(d->mWord)) {
0638                     break;
0639                 }
0640 
0641                 d->mWord.replace(0, 1, d->mWord.at(0).toUpper());
0642                 break;
0643             } else {
0644                 break;
0645             }
0646         }
0647     }
0648 
0649     d->mCursor.setPosition(startPos);
0650     d->mCursor.setPosition(startPos + d->mWord.length(), QTextCursor::KeepAnchor);
0651 }
0652 
0653 bool AutoCorrection::autoFractions() const
0654 {
0655     if (!d->mAutoCorrectionSettings->isAutoFractions()) {
0656         return false;
0657     }
0658 
0659     const QString trimmed = d->mWord.trimmed();
0660     const auto trimmedLength{trimmed.length()};
0661     if (trimmedLength > 3) {
0662         const QChar x = trimmed.at(3);
0663         const uchar xunicode = x.unicode();
0664         if (!(xunicode == '.' || xunicode == ',' || xunicode == '?' || xunicode == '!' || xunicode == ':' || xunicode == ';')) {
0665             return false;
0666         }
0667     } else if (trimmedLength < 3) {
0668         return false;
0669     }
0670 
0671     if (trimmed.startsWith(QLatin1String("1/2"))) {
0672         d->mWord.replace(0, 3, QStringLiteral("½"));
0673     } else if (trimmed.startsWith(QLatin1String("1/4"))) {
0674         d->mWord.replace(0, 3, QStringLiteral("¼"));
0675     } else if (trimmed.startsWith(QLatin1String("3/4"))) {
0676         d->mWord.replace(0, 3, QStringLiteral("¾"));
0677     } else {
0678         return false;
0679     }
0680 
0681     return true;
0682 }
0683 
0684 int AutoCorrection::advancedAutocorrect()
0685 {
0686     if (!d->mAutoCorrectionSettings->isAdvancedAutocorrect()) {
0687         return -1;
0688     }
0689     if (d->mAutoCorrectionSettings->autocorrectEntries().isEmpty()) {
0690         return -1;
0691     }
0692     const QString trimmedWord = d->mWord.trimmed();
0693     if (trimmedWord.isEmpty()) {
0694         return -1;
0695     }
0696     QString actualWord = trimmedWord;
0697 
0698     const int actualWordLength(actualWord.length());
0699     if (actualWordLength < d->mAutoCorrectionSettings->minFindStringLength()) {
0700         return -1;
0701     }
0702     if (actualWordLength > d->mAutoCorrectionSettings->maxFindStringLength()) {
0703         return -1;
0704     }
0705     const int startPos = d->mCursor.selectionStart();
0706     qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << "d->mCursor  " << d->mCursor.selectedText() << " startPos " << startPos;
0707     const int length = d->mWord.length();
0708     // If the last char is punctuation, drop it for now
0709     bool hasPunctuation = false;
0710     const QChar lastChar = actualWord.at(actualWord.length() - 1);
0711     const ushort charUnicode = lastChar.unicode();
0712     if (charUnicode == '.' || charUnicode == ',' || charUnicode == '?' || charUnicode == '!' || charUnicode == ';') {
0713         hasPunctuation = true;
0714         actualWord.chop(1);
0715     } else if (charUnicode == ':' && actualWord.at(0).unicode() != ':') {
0716         hasPunctuation = true;
0717         actualWord.chop(1);
0718     }
0719     QString actualWordWithFirstUpperCase = actualWord;
0720     if (!actualWordWithFirstUpperCase.isEmpty()) {
0721         actualWordWithFirstUpperCase[0] = actualWordWithFirstUpperCase[0].toUpper();
0722     }
0723     QHashIterator<QString, QString> i(d->mAutoCorrectionSettings->autocorrectEntries());
0724     while (i.hasNext()) {
0725         i.next();
0726         const auto key = i.key();
0727         const auto keyLength{key.length()};
0728         if (hasPunctuation) {
0729             // We remove 1 element when we have punctuation
0730             if (keyLength != actualWordLength - 1) {
0731                 continue;
0732             }
0733         } else {
0734             if (keyLength != actualWordLength) {
0735                 continue;
0736             }
0737         }
0738         qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " i.key() " << key << "actual" << actualWord;
0739         if (actualWord.endsWith(key) || actualWord.endsWith(key, Qt::CaseInsensitive) || actualWordWithFirstUpperCase.endsWith(key)) {
0740             int pos = d->mWord.lastIndexOf(key);
0741             qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " pos 1 " << pos << " d->mWord " << d->mWord;
0742             if (pos == -1) {
0743                 pos = actualWord.toLower().lastIndexOf(key);
0744                 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " pos 2 " << pos;
0745                 if (pos == -1) {
0746                     pos = actualWordWithFirstUpperCase.lastIndexOf(key);
0747                     qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " pos 3 " << pos;
0748                     if (pos == -1) {
0749                         continue;
0750                     }
0751                 }
0752             }
0753             QString replacement = i.value();
0754 
0755             // qDebug() << " actualWord " << actualWord << " pos " << pos << " actualWord.size" << actualWord.length() << "actualWordWithFirstUpperCase "
0756             // <<actualWordWithFirstUpperCase; qDebug() << " d->mWord " << d->mWord << " i.key() " << i.key() << "replacement " << replacement; Keep capitalized
0757             // words capitalized. (Necessary to make sure the first letters match???)
0758             const QChar actualWordFirstChar = d->mWord.at(pos);
0759             qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " actualWordFirstChar " << actualWordFirstChar;
0760 
0761             const QChar replacementFirstChar = replacement[0];
0762             if (actualWordFirstChar.isUpper() && replacementFirstChar.isLower()) {
0763                 replacement[0] = replacementFirstChar.toUpper();
0764             } else if (actualWordFirstChar.isLower() && replacementFirstChar.isUpper()) {
0765                 replacement[0] = replacementFirstChar.toLower();
0766             }
0767 
0768             // If a punctuation mark was on the end originally, add it back on
0769             if (hasPunctuation) {
0770                 replacement.append(lastChar);
0771             }
0772 
0773             d->mWord.replace(pos, pos + trimmedWord.length(), replacement);
0774 
0775             // We do replacement here, since the length of new word might be different from length of
0776             // the old world. Length difference might affect other type of autocorrection
0777             d->mCursor.setPosition(startPos);
0778             d->mCursor.setPosition(startPos + length, QTextCursor::KeepAnchor);
0779             d->mCursor.insertText(d->mWord);
0780             qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) << " insert text " << d->mWord << " startPos " << startPos;
0781             d->mCursor.setPosition(startPos); // also restore the selection
0782             const int newPosition = startPos + d->mWord.length();
0783             d->mCursor.setPosition(newPosition, QTextCursor::KeepAnchor);
0784             return newPosition;
0785         }
0786     }
0787     return -1;
0788 }
0789 
0790 void AutoCorrection::replaceTypographicQuotes()
0791 {
0792     // TODO add french quotes support.
0793     /* this method is ported from lib/kotext/KoAutoFormat.cpp KoAutoFormat::doTypographicQuotes
0794      * from Calligra 1.x branch */
0795 
0796     if (!(d->mAutoCorrectionSettings->isReplaceDoubleQuotes() && d->mWord.contains(QLatin1Char('"')))
0797         && !(d->mAutoCorrectionSettings->isReplaceSingleQuotes() && d->mWord.contains(QLatin1Char('\'')))) {
0798         return;
0799     }
0800 
0801     const bool addNonBreakingSpace = (d->mAutoCorrectionSettings->isFrenchLanguage() && d->mAutoCorrectionSettings->isAddNonBreakingSpace());
0802 
0803     // Need to determine if we want a starting or ending quote.
0804     // we use a starting quote in three cases:
0805     //  1. if the previous character is a space
0806     //  2. if the previous character is some kind of opening punctuation (e.g., "(", "[", or "{")
0807     //     a. and the character before that is not an opening quote (so that we get quotations of single characters
0808     //        right)
0809     //  3. if the previous character is an opening quote (so that we get nested quotations right)
0810     //     a. and the character before that is not an opening quote (so that we get quotations of single characters
0811     //         right)
0812     //     b. and the previous quote of a different kind (so that we get empty quotations right)
0813 
0814     bool ending = true;
0815     for (int i = d->mWord.length(); i > 1; --i) {
0816         const QChar c = d->mWord.at(i - 1);
0817         if (c == QLatin1Char('"') || c == QLatin1Char('\'')) {
0818             const bool doubleQuotes = (c == QLatin1Char('"'));
0819             if (i > 2) {
0820                 QChar::Category c1 = d->mWord.at(i - 1).category();
0821 
0822                 // case 1 and 2
0823                 if (c1 == QChar::Separator_Space || c1 == QChar::Separator_Line || c1 == QChar::Separator_Paragraph || c1 == QChar::Punctuation_Open
0824                     || c1 == QChar::Other_Control) {
0825                     ending = false;
0826                 }
0827 
0828                 // case 3
0829                 if (c1 == QChar::Punctuation_InitialQuote) {
0830                     QChar openingQuote;
0831 
0832                     if (doubleQuotes) {
0833                         if (d->mAutoCorrectionSettings->isReplaceDoubleQuotesByFrenchQuotes()) {
0834                             openingQuote = d->mAutoCorrectionSettings->doubleFrenchQuotes().begin;
0835                         } else {
0836                             openingQuote = d->mAutoCorrectionSettings->typographicDoubleQuotes().begin;
0837                         }
0838                     } else {
0839                         openingQuote = d->mAutoCorrectionSettings->typographicSingleQuotes().begin;
0840                     }
0841 
0842                     // case 3b
0843                     if (d->mWord.at(i - 1) != openingQuote) {
0844                         ending = false;
0845                     }
0846                 }
0847             }
0848             // case 2a and 3a
0849             if (i > 3 && !ending) {
0850                 const QChar::Category c2 = (d->mWord.at(i - 2)).category();
0851                 ending = (c2 == QChar::Punctuation_InitialQuote);
0852             }
0853 
0854             if (doubleQuotes && d->mAutoCorrectionSettings->isReplaceDoubleQuotes()) {
0855                 if (ending) {
0856                     const QChar endQuote = d->mAutoCorrectionSettings->isReplaceDoubleQuotesByFrenchQuotes()
0857                         ? d->mAutoCorrectionSettings->doubleFrenchQuotes().end
0858                         : d->mAutoCorrectionSettings->typographicDoubleQuotes().end;
0859                     if (addNonBreakingSpace) {
0860                         d->mWord.replace(i - 1, 2, QString(d->mAutoCorrectionSettings->nonBreakingSpace() + endQuote));
0861                     } else {
0862                         d->mWord[i - 1] = endQuote;
0863                     }
0864                 } else {
0865                     const QChar beginQuote = d->mAutoCorrectionSettings->isReplaceDoubleQuotesByFrenchQuotes()
0866                         ? d->mAutoCorrectionSettings->doubleFrenchQuotes().begin
0867                         : d->mAutoCorrectionSettings->typographicDoubleQuotes().begin;
0868                     if (addNonBreakingSpace) {
0869                         d->mWord.replace(i - 1, 2, QString(d->mAutoCorrectionSettings->nonBreakingSpace() + beginQuote));
0870                     } else {
0871                         d->mWord[i - 1] = beginQuote;
0872                     }
0873                 }
0874             } else if (d->mAutoCorrectionSettings->isReplaceSingleQuotes()) {
0875                 if (ending) {
0876                     if (addNonBreakingSpace) {
0877                         d->mWord.replace(i - 1,
0878                                          2,
0879                                          QString(d->mAutoCorrectionSettings->nonBreakingSpace() + d->mAutoCorrectionSettings->typographicSingleQuotes().end));
0880                     } else {
0881                         d->mWord[i - 1] = d->mAutoCorrectionSettings->typographicSingleQuotes().end;
0882                     }
0883                 } else {
0884                     if (addNonBreakingSpace) {
0885                         d->mWord.replace(i - 1,
0886                                          2,
0887                                          QString(d->mAutoCorrectionSettings->nonBreakingSpace() + d->mAutoCorrectionSettings->typographicSingleQuotes().begin));
0888                     } else {
0889                         d->mWord[i - 1] = d->mAutoCorrectionSettings->typographicSingleQuotes().begin;
0890                     }
0891                 }
0892             }
0893         }
0894     }
0895 
0896     // first character
0897     if (d->mWord.at(0) == QLatin1Char('"') && d->mAutoCorrectionSettings->isReplaceDoubleQuotes()) {
0898         const QChar beginQuote = d->mAutoCorrectionSettings->isReplaceDoubleQuotesByFrenchQuotes()
0899             ? d->mAutoCorrectionSettings->doubleFrenchQuotes().begin
0900             : d->mAutoCorrectionSettings->typographicDoubleQuotes().begin;
0901         d->mWord[0] = beginQuote;
0902         if (addNonBreakingSpace) {
0903             d->mWord.insert(1, d->mAutoCorrectionSettings->nonBreakingSpace());
0904         }
0905     } else if (d->mWord.at(0) == QLatin1Char('\'') && d->mAutoCorrectionSettings->isReplaceSingleQuotes()) {
0906         d->mWord[0] = d->mAutoCorrectionSettings->typographicSingleQuotes().begin;
0907         if (addNonBreakingSpace) {
0908             d->mWord.insert(1, d->mAutoCorrectionSettings->nonBreakingSpace());
0909         }
0910     }
0911 }
0912 
0913 void AutoCorrection::loadGlobalFileName(const QString &fname)
0914 {
0915     d->mAutoCorrectionSettings->loadGlobalFileName(fname);
0916 }
0917 
0918 void AutoCorrection::writeAutoCorrectionXmlFile(const QString &filename)
0919 {
0920     d->mAutoCorrectionSettings->writeAutoCorrectionFile(filename);
0921 }