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 }