8#include "autocorrection.h"
10#include "autocorrectionutils.h"
11#include "textautocorrectionautocorrect_debug.h"
12#include <KColorScheme>
14#include <QStandardPaths>
16#include <QTextDocument>
18using namespace TextAutoCorrectionCore;
20namespace TextAutoCorrectionCore
22class AutoCorrectionPrivate
25 AutoCorrectionPrivate()
26 : mAutoCorrectionSettings(new AutoCorrectionSettings)
30 for (
int i = 1; i <= 7; ++i) {
31 mCacheNameOfDays.
append(locale.dayName(i).toLower());
34 ~AutoCorrectionPrivate()
36 delete mAutoCorrectionSettings;
43 AutoCorrectionSettings *mAutoCorrectionSettings =
nullptr;
47AutoCorrection::AutoCorrection()
48 : d(new AutoCorrectionPrivate)
52AutoCorrection::~AutoCorrection() =
default;
54void AutoCorrection::selectStringOnMaximumSearchString(
QTextCursor &cursor,
int cursorPosition)
59 int pos = qMax(block.
position(), cursorPosition - d->mAutoCorrectionSettings->maxFindStringLength());
67 const int currentPos = (pos - block.
position());
72 bool foundWord =
false;
73 const int textLength(text.
length());
74 for (
int i = currentPos; i < textLength; ++i) {
76 pos = qMin(cursorPosition, pos + 1 + block.
position());
90void AutoCorrection::selectPreviousWord(
QTextCursor &cursor,
int cursorPosition)
100 while (iter !=
string.
end()) {
101 if (iter->isSpace()) {
104 }
else if (pos < cursorPosition) {
118bool AutoCorrection::autocorrect(
bool htmlMode,
QTextDocument &document,
int &position)
120 if (d->mAutoCorrectionSettings->isEnabledAutoCorrection()) {
122 d->mCursor.setPosition(position);
125 if (!singleSpaces()) {
129 int oldPosition = position;
130 selectPreviousWord(d->mCursor, position);
131 d->mWord = d->mCursor.selectedText();
132 if (d->mWord.isEmpty()) {
135 d->mCursor.beginEditBlock();
138 done = autoFormatURLs();
140 done = autoBoldUnderline();
147 superscriptAppendix();
151 done = autoFractions();
158 uppercaseFirstCharOfSentence();
159 fixTwoUppercaseChars();
160 capitalizeWeekDays();
161 replaceTypographicQuotes();
162 if (d->mWord.length() <= 2) {
163 addNonBreakingSpace();
167 if (d->mCursor.selectedText() != d->mWord) {
168 d->mCursor.insertText(d->mWord);
170 position = oldPosition;
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();
180 const int positionEnd(d->mCursor.selectionEnd());
181 d->mCursor.setPosition(d->mCursor.selectionStart() + diffSize);
183 const int newPos = advancedAutocorrect();
185 if (d->mCursor.selectedText() != d->mWord) {
186 d->mCursor.insertText(d->mWord);
194 d->mCursor.endEditBlock();
199void AutoCorrection::readConfig()
201 d->mAutoCorrectionSettings->readConfig();
204void AutoCorrection::writeConfig()
206 d->mAutoCorrectionSettings->writeConfig();
209void AutoCorrection::superscriptAppendix()
211 if (!d->mAutoCorrectionSettings->isSuperScript()) {
215 const QString trimmed = d->mWord.trimmed();
218 const int trimmedLenght(trimmed.
length());
221 while (i != d->mAutoCorrectionSettings->superScriptEntries().constEnd()) {
222 if (i.key() == trimmed) {
223 startPos = d->mCursor.selectionStart() + 1;
224 endPos = startPos - 1 + trimmedLenght;
226 }
else if (i.key() ==
"othernb"_L1) {
227 const int pos = trimmed.
indexOf(i.value());
239 if (!constIter->isNumber()) {
246 if (found && numberLength + i.value().length() == trimmedLenght) {
247 startPos = d->mCursor.selectionStart() + pos;
248 endPos = startPos - pos + trimmedLenght;
256 if (startPos != -1 && endPos != -1) {
267void AutoCorrection::addNonBreakingSpace()
269 if (d->mAutoCorrectionSettings->isAddNonBreakingSpace() && d->mAutoCorrectionSettings->isFrenchLanguage()) {
272 const QChar lastChar = text.
at(d->mCursor.position() - 1 - block.
position());
276 const int pos = d->mCursor.position() - 2 - block.
position();
278 const QChar previousChar = text.
at(pos);
284 d->mCursor.insertText(d->mAutoCorrectionSettings->nonBreakingSpace());
289 const int pos = d->mCursor.position() - 2 - block.
position();
291 const QChar previousChar = text.
at(pos);
294 const int pos = d->mCursor.position() - 3 - block.
position();
296 const QChar previousCharFromDegrees = text.
at(pos);
297 if (previousCharFromDegrees.
isSpace()) {
302 d->mCursor.insertText(d->mAutoCorrectionSettings->nonBreakingSpace());
311bool AutoCorrection::autoBoldUnderline()
313 if (!d->mAutoCorrectionSettings->isAutoBoldUnderline()) {
316 const QString trimmed = d->mWord.trimmed();
318 const auto trimmedLength{trimmed.
length()};
319 if (trimmedLength < 3) {
323 const QChar trimmedFirstChar(trimmed.
at(0));
324 const QChar trimmedLastChar(trimmed.
at(trimmedLength - 1));
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;
334 while (constIter != replacement.
constEnd()) {
335 if (constIter->isLetterOrNumber()) {
336 foundLetterNumber =
true;
343 if (!foundLetterNumber) {
346 d->mCursor.setPosition(startPos);
348 d->mCursor.insertText(replacement);
349 d->mCursor.setPosition(startPos);
353 format.
setFontUnderline(underline ?
true : d->mCursor.charFormat().fontUnderline());
355 format.
setFontStrikeOut(strikeOut ?
true : d->mCursor.charFormat().fontStrikeOut());
356 d->mCursor.mergeCharFormat(format);
359 d->mWord = d->mCursor.selectedText();
368QColor AutoCorrection::linkColor()
370 if (!d->mLinkColor.isValid()) {
373 return d->mLinkColor;
376AutoCorrectionSettings *AutoCorrection::autoCorrectionSettings()
const
378 return d->mAutoCorrectionSettings;
381void AutoCorrection::setAutoCorrectionSettings(AutoCorrectionSettings *newAutoCorrectionSettings)
383 if (d->mAutoCorrectionSettings != newAutoCorrectionSettings) {
384 delete d->mAutoCorrectionSettings;
386 d->mAutoCorrectionSettings = newAutoCorrectionSettings;
389bool AutoCorrection::autoFormatURLs()
391 if (!d->mAutoCorrectionSettings->isAutoFormatUrl()) {
400 const QString trimmed = d->mWord.trimmed();
401 const int startPos = d->mCursor.selectionStart();
402 d->mCursor.setPosition(startPos);
412 d->mCursor.mergeCharFormat(format);
414 d->mWord = d->mCursor.selectedText();
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://");
443 LinkType linkType = UNCLASSIFIED;
450 for (
const QString &scheme : schemes) {
454 contentPos = pos + scheme.length();
459 if (linkType == UNCLASSIFIED) {
463 contentPos = pos + 4;
466 if (linkType == UNCLASSIFIED) {
470 contentPos = pos + 4;
473 if (linkType == UNCLASSIFIED) {
475 if (separatorPos != -1) {
476 pos = separatorPos - 1;
489 contentPos = separatorPos + 1;
495 if (linkType != UNCLASSIFIED) {
498 int lastPos = word.
length() - 1;
503 if (lastPos < contentPos) {
527void AutoCorrection::fixTwoUppercaseChars()
529 if (!d->mAutoCorrectionSettings->isFixTwoUppercaseChars()) {
532 if (d->mWord.length() <= 2) {
536 if (d->mAutoCorrectionSettings->twoUpperLetterExceptions().contains(d->mWord.trimmed())) {
540 const QChar firstChar = d->mWord.at(0);
541 const QChar secondChar = d->mWord.at(1);
544 const QChar thirdChar = d->mWord.at(2);
547 d->mWord.replace(1, 1, secondChar.
toLower());
553bool AutoCorrection::singleSpaces()
const
555 if (!d->mAutoCorrectionSettings->isSingleSpaces()) {
558 if (!d->mCursor.atBlockStart()) {
569void AutoCorrection::capitalizeWeekDays()
571 if (!d->mAutoCorrectionSettings->isCapitalizeWeekDays()) {
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);
585bool AutoCorrection::excludeToUppercase(
const QString &word)
const
594void AutoCorrection::uppercaseFirstCharOfSentence()
596 if (!d->mAutoCorrectionSettings->isUppercaseFirstCharOfSentence()) {
600 int startPos = d->mCursor.selectionStart();
603 d->mCursor.setPosition(block.
position());
606 int position = d->mCursor.selectionEnd();
608 const QString text = d->mCursor.selectedText();
611 if (!excludeToUppercase(d->mWord)) {
612 d->mWord.replace(0, 1, d->mWord.at(0).toUpper());
619 while (constIter != text.
begin() && constIter->isSpace()) {
626 while (constIter != text.
constBegin() && !(constIter->isLetter())) {
630 selectPreviousWord(d->mCursor, --position);
631 const QString prevWord = d->mCursor.selectedText();
634 if (d->mAutoCorrectionSettings->upperCaseExceptions().contains(prevWord.
trimmed())) {
637 if (excludeToUppercase(d->mWord)) {
641 d->mWord.replace(0, 1, d->mWord.at(0).toUpper());
649 d->mCursor.setPosition(startPos);
653bool AutoCorrection::autoFractions()
const
655 if (!d->mAutoCorrectionSettings->isAutoFractions()) {
659 const QString trimmed = d->mWord.trimmed();
660 const auto trimmedLength{trimmed.
length()};
661 if (trimmedLength > 3) {
663 const uchar xunicode = x.
unicode();
664 if (!(xunicode ==
'.' || xunicode ==
',' || xunicode ==
'?' || xunicode ==
'!' || xunicode ==
':' || xunicode ==
';')) {
667 }
else if (trimmedLength < 3) {
672 d->mWord.replace(0, 3, QStringLiteral(
"½"));
674 d->mWord.replace(0, 3, QStringLiteral(
"¼"));
676 d->mWord.replace(0, 3, QStringLiteral(
"¾"));
684int AutoCorrection::advancedAutocorrect()
686 if (!d->mAutoCorrectionSettings->isAdvancedAutocorrect()) {
689 if (d->mAutoCorrectionSettings->autocorrectEntries().isEmpty()) {
692 const QString trimmedWord = d->mWord.trimmed();
696 QString actualWord = trimmedWord;
698 const int actualWordLength(actualWord.
length());
699 if (actualWordLength < d->mAutoCorrectionSettings->minFindStringLength()) {
702 if (actualWordLength > d->mAutoCorrectionSettings->maxFindStringLength()) {
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();
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;
715 }
else if (charUnicode ==
':' && actualWord.
at(0).
unicode() !=
':') {
716 hasPunctuation =
true;
719 QString actualWordWithFirstUpperCase = actualWord;
720 if (!actualWordWithFirstUpperCase.
isEmpty()) {
721 actualWordWithFirstUpperCase[0] = actualWordWithFirstUpperCase[0].
toUpper();
724 while (i.hasNext()) {
726 const auto key = i.key();
727 const auto keyLength{key.length()};
728 if (hasPunctuation) {
730 if (keyLength != actualWordLength - 1) {
734 if (keyLength != actualWordLength) {
738 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) <<
" i.key() " << key <<
"actual" << actualWord;
740 int pos = d->mWord.lastIndexOf(key);
741 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) <<
" pos 1 " << pos <<
" d->mWord " << d->mWord;
743 pos = actualWord.toLower().lastIndexOf(key);
744 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) <<
" pos 2 " << pos;
746 pos = actualWordWithFirstUpperCase.
lastIndexOf(key);
747 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) <<
" pos 3 " << pos;
753 QString replacement = i.value();
758 const QChar actualWordFirstChar = d->mWord.at(pos);
759 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) <<
" actualWordFirstChar " << actualWordFirstChar;
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();
769 if (hasPunctuation) {
770 replacement.
append(lastChar);
773 d->mWord.replace(pos, pos + trimmedWord.
length(), replacement);
777 d->mCursor.setPosition(startPos);
779 d->mCursor.insertText(d->mWord);
780 qCDebug(TEXTAUTOCORRECTION_AUTOCORRECT_LOG) <<
" insert text " << d->mWord <<
" startPos " << startPos;
781 d->mCursor.setPosition(startPos);
782 const int newPosition = startPos + d->mWord.length();
790void AutoCorrection::replaceTypographicQuotes()
796 if (!(d->mAutoCorrectionSettings->isReplaceDoubleQuotes() && d->mWord.contains(
QLatin1Char(
'"')))
797 && !(d->mAutoCorrectionSettings->isReplaceSingleQuotes() && d->mWord.contains(
QLatin1Char(
'\'')))) {
801 const bool addNonBreakingSpace = (d->mAutoCorrectionSettings->isFrenchLanguage() && d->mAutoCorrectionSettings->isAddNonBreakingSpace());
815 for (
int i = d->mWord.length(); i > 1; --i) {
816 const QChar c = d->mWord.at(i - 1);
833 if (d->mAutoCorrectionSettings->isReplaceDoubleQuotesByFrenchQuotes()) {
834 openingQuote = d->mAutoCorrectionSettings->doubleFrenchQuotes().begin;
836 openingQuote = d->mAutoCorrectionSettings->typographicDoubleQuotes().begin;
839 openingQuote = d->mAutoCorrectionSettings->typographicSingleQuotes().begin;
843 if (d->mWord.at(i - 1) != openingQuote) {
849 if (i > 3 && !ending) {
854 if (doubleQuotes && d->mAutoCorrectionSettings->isReplaceDoubleQuotes()) {
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));
862 d->mWord[i - 1] = endQuote;
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));
871 d->mWord[i - 1] = beginQuote;
874 }
else if (d->mAutoCorrectionSettings->isReplaceSingleQuotes()) {
876 if (addNonBreakingSpace) {
877 d->mWord.replace(i - 1,
879 QString(d->mAutoCorrectionSettings->nonBreakingSpace() + d->mAutoCorrectionSettings->typographicSingleQuotes().end));
881 d->mWord[i - 1] = d->mAutoCorrectionSettings->typographicSingleQuotes().end;
884 if (addNonBreakingSpace) {
885 d->mWord.replace(i - 1,
887 QString(d->mAutoCorrectionSettings->nonBreakingSpace() + d->mAutoCorrectionSettings->typographicSingleQuotes().begin));
889 d->mWord[i - 1] = d->mAutoCorrectionSettings->typographicSingleQuotes().begin;
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());
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());
913void AutoCorrection::loadGlobalFileName(
const QString &fname)
915 d->mAutoCorrectionSettings->loadGlobalFileName(fname);
918void AutoCorrection::writeAutoCorrectionXmlFile(
const QString &filename)
920 d->mAutoCorrectionSettings->writeAutoCorrectionFile(filename);
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)
void append(QList< T > &&value)
void reserve(qsizetype size)
QString & append(QChar ch)
const QChar at(qsizetype position) const const
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)
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 mergeCharFormat(const QTextCharFormat &modifier)
void setPosition(int pos, MoveMode m)
void setForeground(const QBrush &brush)