2018-12-30 20:18:35 -05:00
|
|
|
/**
|
|
|
|
* This file is part of JS8Call.
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
* (C) 2018 Jordan Sherer <kn4crd@gmail.com> - All Rights Reserved
|
|
|
|
*
|
|
|
|
**/
|
|
|
|
|
|
|
|
#include "jsc_checker.h"
|
|
|
|
|
|
|
|
#include <QTextEdit>
|
|
|
|
#include <QTextBlock>
|
|
|
|
#include <QTextCursor>
|
|
|
|
#include <QTextDocument>
|
|
|
|
#include <QTextLayout>
|
|
|
|
#include <QDebug>
|
|
|
|
|
|
|
|
#include "jsc.h"
|
|
|
|
#include "varicode.h"
|
|
|
|
|
|
|
|
const int CORRECT = QTextFormat::UserProperty + 10;
|
|
|
|
const QString ALPHABET = { "ABCDEFGHIJKLMNOPQRSTUVWXYZ" };
|
|
|
|
|
|
|
|
JSCChecker::JSCChecker(QObject *parent) :
|
|
|
|
QObject(parent)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
bool cursorHasProperty(const QTextCursor &cursor, int property){
|
|
|
|
if(property < QTextFormat::UserProperty) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if(cursor.charFormat().intProperty(property) == 1) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
const QList<QTextLayout::FormatRange>& formats = cursor.block().layout()->additionalFormats();
|
|
|
|
int pos = cursor.positionInBlock();
|
|
|
|
foreach(const QTextLayout::FormatRange& range, formats) {
|
|
|
|
if(pos > range.start && pos <= range.start + range.length && range.format.intProperty(property) == 1) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString nextChar(QTextCursor c){
|
|
|
|
QTextCursor cur(c);
|
|
|
|
cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
|
|
return cur.selectedText();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool isNumeric(QString s){
|
|
|
|
return s.indexOf(QRegExp("^\\d+$")) == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool isWordChar(QString ch){
|
|
|
|
return ch.contains(QRegExp("^\\w$"));
|
|
|
|
}
|
|
|
|
|
|
|
|
void JSCChecker::checkRange(QTextEdit* edit, int start, int end)
|
|
|
|
{
|
|
|
|
if(end == -1){
|
|
|
|
QTextCursor tmpCursor(edit->textCursor());
|
|
|
|
tmpCursor.movePosition(QTextCursor::End);
|
|
|
|
end = tmpCursor.position();
|
|
|
|
}
|
|
|
|
|
|
|
|
// stop contentsChange signals from being emitted due to changed charFormats
|
|
|
|
edit->document()->blockSignals(true);
|
|
|
|
|
|
|
|
qDebug() << "checking range " << start << " - " << end;
|
|
|
|
|
|
|
|
QTextCharFormat errorFmt;
|
|
|
|
errorFmt.setFontUnderline(true);
|
|
|
|
errorFmt.setUnderlineColor(Qt::red);
|
|
|
|
errorFmt.setUnderlineStyle(QTextCharFormat::WaveUnderline);
|
|
|
|
QTextCharFormat defaultFormat = QTextCharFormat();
|
|
|
|
|
|
|
|
auto cursor = edit->textCursor();
|
|
|
|
|
|
|
|
cursor.beginEditBlock();
|
|
|
|
{
|
|
|
|
cursor.setPosition(start);
|
|
|
|
while(cursor.position() < end) {
|
|
|
|
bool correct = false;
|
|
|
|
|
|
|
|
cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
|
|
|
if(cursor.selectedText() == "@"){
|
|
|
|
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
|
|
|
cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(cursorHasProperty(cursor, CORRECT)){
|
|
|
|
correct = true;
|
|
|
|
} else {
|
|
|
|
QString word = cursor.selectedText();
|
|
|
|
|
|
|
|
// three or less is always "correct"
|
|
|
|
if(word.length() < 4 || isNumeric(word)){
|
|
|
|
correct = true;
|
|
|
|
} else {
|
|
|
|
bool found = false;
|
|
|
|
quint32 index = JSC::lookup(word, &found);
|
|
|
|
if(found){
|
|
|
|
correct = JSC::map[index].size == word.length();
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!correct){
|
|
|
|
correct = Varicode::isValidCallsign(word, nullptr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(correct){
|
|
|
|
QTextCharFormat fmt = cursor.charFormat();
|
|
|
|
fmt.setFontUnderline(defaultFormat.fontUnderline());
|
|
|
|
fmt.setUnderlineColor(defaultFormat.underlineColor());
|
|
|
|
fmt.setUnderlineStyle(defaultFormat.underlineStyle());
|
|
|
|
cursor.setCharFormat(fmt);
|
|
|
|
} else {
|
|
|
|
cursor.mergeCharFormat(errorFmt);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Go to next word start
|
|
|
|
//while(cursor.position() < end && !isWordChar(nextChar(cursor))){
|
|
|
|
// cursor.movePosition(QTextCursor::NextCharacter);
|
|
|
|
//}
|
|
|
|
cursor.movePosition(QTextCursor::NextCharacter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cursor.endEditBlock();
|
|
|
|
|
|
|
|
edit->document()->blockSignals(false);
|
|
|
|
}
|
|
|
|
|
2018-12-31 15:14:48 -05:00
|
|
|
QSet<QString> oneEdit(QString word, bool includeAdditions, bool includeDeletions){
|
|
|
|
QSet<QString> all;
|
|
|
|
|
|
|
|
// 1-edit distance words (i.e., prefixed/suffixed/edited characters)
|
|
|
|
for(int i = 0; i < 26; i++){
|
|
|
|
if(includeAdditions){
|
|
|
|
auto prefixed = ALPHABET.mid(i, 1) + word;
|
|
|
|
all.insert(prefixed);
|
|
|
|
|
|
|
|
auto suffixed = word + ALPHABET.mid(i, 1);
|
|
|
|
all.insert(suffixed);
|
|
|
|
}
|
|
|
|
|
|
|
|
for(int j = 0; j < word.length(); j++){
|
|
|
|
auto edited = word.mid(0, j) + ALPHABET.mid(i, 1) + word.mid(j + 1, word.length() - j);
|
|
|
|
all.insert(edited);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 1-edit distance words (i.e., removed characters)
|
|
|
|
if(includeDeletions){
|
|
|
|
for(int j = 0; j < word.length(); j++){
|
|
|
|
auto deleted = word.mid(0, j) + word.mid(j + 1, word.length() - j);
|
|
|
|
all.insert(deleted);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return all;
|
|
|
|
}
|
|
|
|
|
|
|
|
QMap<quint32, QString> candidates(QString word, bool includeTwoEdits){
|
|
|
|
// one edit
|
|
|
|
QSet<QString> one = oneEdit(word, true, true);
|
|
|
|
|
|
|
|
// two edits
|
|
|
|
QSet<QString> two;
|
|
|
|
if(includeTwoEdits){
|
|
|
|
foreach(auto w, one){
|
|
|
|
two |= oneEdit(w, false, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// existence check
|
|
|
|
QMap<quint32, QString> m;
|
|
|
|
|
|
|
|
quint32 index;
|
|
|
|
foreach(auto w, one | two){
|
|
|
|
if(JSC::exists(w, &index)){
|
|
|
|
m[index] = w;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return m;
|
|
|
|
}
|
|
|
|
|
2018-12-30 20:18:35 -05:00
|
|
|
QStringList JSCChecker::suggestions(QString word, int n, bool *pFound){
|
|
|
|
QStringList s;
|
|
|
|
|
|
|
|
qDebug() << "computing suggestions for word" << word;
|
|
|
|
|
|
|
|
QMap<quint32, QString> m;
|
|
|
|
|
|
|
|
bool prefixFound = false;
|
|
|
|
|
|
|
|
// lookup actual word prefix that is not a single character
|
|
|
|
quint32 index = JSC::lookup(word, &prefixFound);
|
|
|
|
if(prefixFound){
|
|
|
|
auto t = JSC::map[index];
|
|
|
|
if(t.size > 1){
|
|
|
|
m[index] = QString::fromLatin1(t.str, t.size);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-31 15:14:48 -05:00
|
|
|
// compute suggestion candidates
|
|
|
|
m.unite(candidates(word, false));
|
2018-12-30 20:18:35 -05:00
|
|
|
|
|
|
|
// return in order of probability (i.e., index rank)
|
|
|
|
int i = 0;
|
2018-12-31 15:14:48 -05:00
|
|
|
foreach(auto key, m.uniqueKeys()){
|
2018-12-30 20:18:35 -05:00
|
|
|
if(i >= n){
|
|
|
|
break;
|
|
|
|
}
|
2018-12-31 15:14:48 -05:00
|
|
|
qDebug() << "suggest" << m[key] << key;
|
2018-12-30 20:18:35 -05:00
|
|
|
s.append(m[key]);
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(pFound) *pFound = prefixFound;
|
|
|
|
|
|
|
|
return s;
|
|
|
|
}
|