From ec0b962badc3383195fc8a1afd7d8c7db9220b8a Mon Sep 17 00:00:00 2001 From: "B.Melnik" Date: Fri, 6 Aug 2021 15:41:48 +0300 Subject: [PATCH] feat(Spellchecker): Add spellchecker class --- .../lib/include/DOtherSide/DosSpellchecker.h | 60 ++++++ vendor/DOtherSide/lib/src/DOtherSide.cpp | 2 + vendor/DOtherSide/lib/src/DosSpellchecker.cpp | 202 ++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 vendor/DOtherSide/lib/include/DOtherSide/DosSpellchecker.h create mode 100644 vendor/DOtherSide/lib/src/DosSpellchecker.cpp diff --git a/vendor/DOtherSide/lib/include/DOtherSide/DosSpellchecker.h b/vendor/DOtherSide/lib/include/DOtherSide/DosSpellchecker.h new file mode 100644 index 0000000000..1918a941c7 --- /dev/null +++ b/vendor/DOtherSide/lib/include/DOtherSide/DosSpellchecker.h @@ -0,0 +1,60 @@ +#ifndef DOSSPELLCHECKER_H +#define DOSSPELLCHECKER_H + +#include +#include +#include +#include + +#ifdef Q_OS_MACOS +class Hunspell; +#endif +class QTextCodec; + +class SpellChecker : public QSyntaxHighlighter +{ + Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + Q_PROPERTY(QString displayText READ displayText NOTIFY displayTextChanged) + Q_PROPERTY(QQuickTextDocument* document READ textDocument WRITE setTextDocument NOTIFY textDocumentChanged) +public: + explicit SpellChecker(QObject *parent = nullptr); + ~SpellChecker(); + + Q_INVOKABLE bool spell(const QString& word); + Q_INVOKABLE QVariantList suggest(const QString &word); + Q_INVOKABLE void ignoreWord(const QString &word); + Q_INVOKABLE void addToUserWordlist(const QString &word); + + void setText(const QString& text); + const QString& text() const; + + const QString& displayText() const; + + QQuickTextDocument* textDocument() const; + void setTextDocument(QQuickTextDocument* document); + +signals: + void textChanged(); + void displayTextChanged(); + void textDocumentChanged(); + +protected: + void highlightBlock(const QString &text) final; + +private: + void makeDisplayText(const QString& text); + +private: + QString m_text; + QString m_displayText; + QString m_dictionaryPath; + + QQuickTextDocument *m_document; +#ifdef Q_OS_MACOS + Hunspell *m_hunspell; +#endif + QTextCodec *m_codec; +}; + +#endif // DOSSPELLCHECKER_H diff --git a/vendor/DOtherSide/lib/src/DOtherSide.cpp b/vendor/DOtherSide/lib/src/DOtherSide.cpp index 9d4b20d6d7..f86699d40a 100644 --- a/vendor/DOtherSide/lib/src/DOtherSide.cpp +++ b/vendor/DOtherSide/lib/src/DOtherSide.cpp @@ -65,6 +65,7 @@ #include "DOtherSide/StatusEvents/StatusDockShowAppEvent.h" #include "DOtherSide/StatusEvents/StatusOSThemeEvent.h" #include "DOtherSide/StatusNotification/StatusOSNotification.h" +#include "DOtherSide/DosSpellchecker.h" namespace { @@ -73,6 +74,7 @@ void register_meta_types() qRegisterMetaType>(); qmlRegisterType("DotherSide", 0 , 1, "StatusWindow"); qmlRegisterType("DotherSide", 0 , 1, "StatusSyntaxHighlighter"); + qmlRegisterType("DotherSide", 0, 1, "SpellChecker"); } } diff --git a/vendor/DOtherSide/lib/src/DosSpellchecker.cpp b/vendor/DOtherSide/lib/src/DosSpellchecker.cpp new file mode 100644 index 0000000000..ff3c395b65 --- /dev/null +++ b/vendor/DOtherSide/lib/src/DosSpellchecker.cpp @@ -0,0 +1,202 @@ +#include "DosSpellchecker.h" + +#include "hunspell.hxx" +#include +#include +#include +#include + +#include +#include +#include + +#include + +SpellChecker::SpellChecker(QObject *parent) + : QSyntaxHighlighter(parent) +{ + auto language = QLocale::system().bcp47Name(); + +#ifdef Q_OS_MACOS + QString dictFile = QApplication::applicationDirPath() + + "/dictionaries/" + language + "/index.dic"; + QString affixFile = QApplication::applicationDirPath() + + "/dictionaries/" + language + "/index.aff"; + QByteArray dictFilePathBA = dictFile.toLocal8Bit(); + QByteArray affixFilePathBA = affixFile.toLocal8Bit(); + m_hunspell = new Hunspell(affixFilePathBA.constData(), + dictFilePathBA.constData()); + + // detect encoding analyzing the SET option in the affix file + auto encoding = QStringLiteral("ISO8859-15"); + QFile _affixFile(affixFile); + if (_affixFile.open(QIODevice::ReadOnly)) { + QTextStream stream(&_affixFile); + QRegularExpression enc_detector( + QStringLiteral("^\\s*SET\\s+([A-Z0-9\\-]+)\\s*"), + QRegularExpression::CaseInsensitiveOption); + QString sLine; + QRegularExpressionMatch match; + while (!stream.atEnd()) { + sLine = stream.readLine(); + if (sLine.isEmpty()) { continue; } + match = enc_detector.match(sLine); + if (match.hasMatch()) { + encoding = match.captured(1); + qDebug() << "Encoding set to " + encoding; + break; + } + } + _affixFile.close(); + } + m_codec = QTextCodec::codecForName(encoding.toLatin1().constData()); + + QString userDict = "userDict_" + language + ".txt"; + + if (!userDict.isEmpty()) { + QFile userDictonaryFile(userDict); + if (userDictonaryFile.open(QIODevice::ReadOnly)) { + QTextStream stream(&userDictonaryFile); + for (QString word = stream.readLine(); + !word.isEmpty(); + word = stream.readLine()) + ignoreWord(word); + userDictonaryFile.close(); + } else { + qWarning() << "User dictionary in " << userDict + << "could not be opened"; + } + } else { + qDebug() << "User dictionary not set."; + } +#endif +} + +SpellChecker::~SpellChecker() +{ +#ifdef Q_OS_MACOS + delete m_hunspell; +#endif +} + +bool SpellChecker::spell(const QString &word) +{ +#ifdef Q_OS_MACOS + return m_hunspell->spell(m_codec->fromUnicode(word).toStdString()); +#else + return true; +#endif +} + +QVariantList SpellChecker::suggest(const QString &word) +{ + int numSuggestions = 0; + QVariantList suggestions; +#ifdef Q_OS_MACOS + std::vector wordlist; + wordlist = m_hunspell->suggest(m_codec->fromUnicode(word).toStdString()); + + numSuggestions = static_cast(wordlist.size()); + if (numSuggestions > 0) { + suggestions.reserve(numSuggestions); + for (int i = 0; i < numSuggestions; i++) { + suggestions << m_codec->toUnicode( + QByteArray::fromStdString(wordlist[i])); + } + } +#endif + + return suggestions; +} + +void SpellChecker::ignoreWord(const QString &word) +{ +#ifdef Q_OS_MACOS + m_hunspell->add(m_codec->fromUnicode(word).constData()); +#endif +} + +void SpellChecker::addToUserWordlist(const QString &word) +{ +#ifdef Q_OS_MACOS + auto language = QLocale::scriptToString(QLocale::system().script()); + + QString userDict = "userDict_" + language + ".txt"; + if (!userDict.isEmpty()) { + QFile userDictonaryFile(userDict); + if (userDictonaryFile.open(QIODevice::Append)) { + QTextStream stream(&userDictonaryFile); + stream << word << "\n"; + userDictonaryFile.close(); + } else { + qWarning() << "User dictionary in " << userDict + << "could not be opened for appending a new word"; + } + } else { + qDebug() << "User dictionary not set."; + } +#endif +} + +void SpellChecker::setText(const QString &text) +{ + if (m_text != text) { + m_text = text; + emit textChanged(); + makeDisplayText(m_text); + } +} + +const QString &SpellChecker::text() const +{ + return m_text; +} + +const QString &SpellChecker::displayText() const +{ + return m_displayText; +} + +QQuickTextDocument *SpellChecker::textDocument() const +{ + return m_document; +} + +void SpellChecker::setTextDocument(QQuickTextDocument *document) +{ + if (m_document != document) { + m_document = document; + setDocument(m_document->textDocument()); + emit textDocumentChanged(); + } +} + +void SpellChecker::highlightBlock(const QString &text) +{ + QTextCharFormat format; + format.setFontUnderline(true); + + QRegularExpression expression("\\S+"); + QRegularExpressionMatchIterator i = expression.globalMatch(text); + while(i.hasNext()) { + QRegularExpressionMatch match = i.next(); + if (!spell(match.captured())) { + setFormat(match.capturedStart(), match.capturedLength(), format); + } + } +} + +void SpellChecker::makeDisplayText(const QString &text) +{ + auto words = text.split(" "); + QString gPattern = "%1"; + + m_displayText = ""; // todo optimize delta + for (const auto& word: qAsConst(words)) { + if (!spell(word)) { + m_displayText.append(" " + gPattern.arg(word)); + } else { + m_displayText.append(" " + word); + } + } + + emit displayTextChanged(); +}