File indexing completed on 2024-05-19 05:05:21

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2023 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include "value.h"
0021 
0022 #include <typeinfo>
0023 
0024 #include <QSet>
0025 #include <QString>
0026 #include <QStringList>
0027 #include <QRegularExpression>
0028 
0029 #ifdef HAVE_KF
0030 #include <KSharedConfig>
0031 #include <KConfigGroup>
0032 #endif // HAVE_KF
0033 #ifdef HAVE_KFI18N
0034 #include <KLocalizedString>
0035 #else // HAVE_KFI18N
0036 #include <QObject>
0037 #define i18n(text) QObject::tr(text)
0038 #endif // HAVE_KFI18N
0039 
0040 #include <Preferences>
0041 #include "logging_data.h"
0042 
0043 quint64 ValueItem::internalIdCounter = 0;
0044 
0045 uint qHash(const QSharedPointer<ValueItem> &valueItem)
0046 {
0047     return qHash(valueItem->id());
0048 }
0049 
0050 const QRegularExpression ValueItem::ignoredInSorting(QStringLiteral("[{}\\\\]+"));
0051 
0052 ValueItem::ValueItem()
0053         : internalId(++internalIdCounter)
0054 {
0055     /// nothing
0056 }
0057 
0058 ValueItem::~ValueItem()
0059 {
0060     /// nothing
0061 }
0062 
0063 quint64 ValueItem::id() const
0064 {
0065     return internalId;
0066 }
0067 
0068 bool ValueItem::operator!=(const ValueItem &other) const
0069 {
0070     return !operator ==(other);
0071 }
0072 
0073 Keyword::Keyword(const Keyword &other)
0074         : m_text(other.m_text)
0075 {
0076     /// nothing
0077 }
0078 
0079 Keyword::Keyword(const QString &text)
0080         : m_text(text)
0081 {
0082     /// nothing
0083 }
0084 
0085 void Keyword::setText(const QString &text)
0086 {
0087     m_text = text;
0088 }
0089 
0090 QString Keyword::text() const
0091 {
0092     return m_text;
0093 }
0094 
0095 void Keyword::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
0096 {
0097     if (replaceMode == ValueItem::ReplaceMode::AnySubstring)
0098         m_text = m_text.replace(before, after);
0099     else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch && m_text == before)
0100         m_text = after;
0101 }
0102 
0103 bool Keyword::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
0104 {
0105     const QString text = QString(m_text).remove(ignoredInSorting);
0106     return text.contains(pattern, caseSensitive);
0107 }
0108 
0109 bool Keyword::operator==(const ValueItem &other) const
0110 {
0111     const Keyword *otherKeyword = dynamic_cast<const Keyword *>(&other);
0112     if (otherKeyword != nullptr) {
0113         return otherKeyword->text() == text();
0114     } else
0115         return false;
0116 }
0117 
0118 bool Keyword::isKeyword(const ValueItem &other) {
0119     return typeid(other) == typeid(Keyword);
0120 }
0121 
0122 
0123 Person::Person(const QString &firstName, const QString &lastName, const QString &suffix)
0124         : m_firstName(firstName), m_lastName(lastName), m_suffix(suffix)
0125 {
0126     /// nothing
0127 }
0128 
0129 Person::Person(const Person &other)
0130         : m_firstName(other.firstName()), m_lastName(other.lastName()), m_suffix(other.suffix())
0131 {
0132     /// nothing
0133 }
0134 
0135 QString Person::firstName() const
0136 {
0137     return m_firstName;
0138 }
0139 
0140 QString Person::lastName() const
0141 {
0142     return m_lastName;
0143 }
0144 
0145 QString Person::suffix() const
0146 {
0147     return m_suffix;
0148 }
0149 
0150 void Person::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
0151 {
0152     if (replaceMode == ValueItem::ReplaceMode::AnySubstring) {
0153         m_firstName = m_firstName.replace(before, after);
0154         m_lastName = m_lastName.replace(before, after);
0155         m_suffix = m_suffix.replace(before, after);
0156     } else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch) {
0157         if (m_firstName == before)
0158             m_firstName = after;
0159         if (m_lastName == before)
0160             m_lastName = after;
0161         if (m_suffix == before)
0162             m_suffix = after;
0163     }
0164 }
0165 
0166 bool Person::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
0167 {
0168     const QString firstName = QString(m_firstName).remove(ignoredInSorting);
0169     const QString lastName = QString(m_lastName).remove(ignoredInSorting);
0170     const QString suffix = QString(m_suffix).remove(ignoredInSorting);
0171 
0172     return firstName.contains(pattern, caseSensitive) || lastName.contains(pattern, caseSensitive) || suffix.contains(pattern, caseSensitive) || QString(QStringLiteral("%1 %2|%2, %1")).arg(firstName, lastName).contains(pattern, caseSensitive);
0173 }
0174 
0175 bool Person::operator==(const ValueItem &other) const
0176 {
0177     const Person *otherPerson = dynamic_cast<const Person *>(&other);
0178     if (otherPerson != nullptr) {
0179         return otherPerson->firstName() == firstName() && otherPerson->lastName() == lastName() && otherPerson->suffix() == suffix();
0180     } else
0181         return false;
0182 }
0183 
0184 QString Person::transcribePersonName(const Person *person, const QString &formatting)
0185 {
0186     return transcribePersonName(formatting, person->firstName(), person->lastName(), person->suffix());
0187 }
0188 
0189 QString Person::transcribePersonName(const QString &formatting, const QString &firstName, const QString &lastName, const QString &suffix)
0190 {
0191     QString result = formatting;
0192     int p1 = -1, p2 = -1, p3 = -1;
0193     while ((p1 = result.indexOf(QLatin1Char('<'))) >= 0 && (p2 = result.indexOf(QLatin1Char('>'), p1 + 1)) >= 0 && (p3 = result.indexOf(QLatin1Char('%'), p1)) >= 0 && p3 < p2) {
0194         QString insert;
0195         switch (result[p3 + 1].toLatin1()) {
0196         case 'f':
0197             insert = firstName;
0198             break;
0199         case 'l':
0200             insert = lastName;
0201             break;
0202         case 's':
0203             insert = suffix;
0204             break;
0205         }
0206 
0207         if (!insert.isEmpty())
0208             insert = result.mid(p1 + 1, p3 - p1 - 1) + insert + result.mid(p3 + 2, p2 - p3 - 2);
0209 
0210         result = result.left(p1) + insert + result.mid(p2 + 1);
0211     }
0212     return result;
0213 }
0214 
0215 bool Person::isPerson(const ValueItem &other) {
0216     return typeid(other) == typeid(Person);
0217 }
0218 
0219 QDebug operator<<(QDebug dbg, const Person *person) {
0220     dbg.nospace() << "Person " << Person::transcribePersonName(person, Preferences::defaultPersonNameFormat);
0221     return dbg;
0222 }
0223 
0224 
0225 MacroKey::MacroKey(const MacroKey &other)
0226         : m_text(other.m_text)
0227 {
0228     /// nothing
0229 }
0230 
0231 MacroKey::MacroKey(const QString &text)
0232         : m_text(text)
0233 {
0234     /// nothing
0235 }
0236 
0237 void MacroKey::setText(const QString &text)
0238 {
0239     m_text = text;
0240 }
0241 
0242 QString MacroKey::text() const
0243 {
0244     return m_text;
0245 }
0246 
0247 bool MacroKey::isValid()
0248 {
0249     const QString t = text();
0250     static const QRegularExpression validMacroKey(QStringLiteral("^[a-z][-.:/+_a-z0-9]*$|^[0-9]+$"), QRegularExpression::CaseInsensitiveOption);
0251     const QRegularExpressionMatch match = validMacroKey.match(t);
0252     return match.hasMatch() && match.captured(0) == t;
0253 }
0254 
0255 void MacroKey::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
0256 {
0257     if (replaceMode == ValueItem::ReplaceMode::AnySubstring)
0258         m_text = m_text.replace(before, after);
0259     else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch && m_text == before)
0260         m_text = after;
0261 }
0262 
0263 bool MacroKey::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
0264 {
0265     const QString text = QString(m_text).remove(ignoredInSorting);
0266     return text.contains(pattern, caseSensitive);
0267 }
0268 
0269 bool MacroKey::operator==(const ValueItem &other) const
0270 {
0271     const MacroKey *otherMacroKey = dynamic_cast<const MacroKey *>(&other);
0272     if (otherMacroKey != nullptr) {
0273         return otherMacroKey->text() == text();
0274     } else
0275         return false;
0276 }
0277 
0278 bool MacroKey::isMacroKey(const ValueItem &other) {
0279     return typeid(other) == typeid(MacroKey);
0280 }
0281 
0282 QDebug operator<<(QDebug dbg, const MacroKey &macrokey) {
0283     dbg.nospace() << "MacroKey " << macrokey.text();
0284     return dbg;
0285 }
0286 
0287 
0288 PlainText::PlainText(const PlainText &other)
0289         : m_text(other.text())
0290 {
0291     /// nothing
0292 }
0293 
0294 PlainText::PlainText(const QString &text)
0295         : m_text(text)
0296 {
0297     /// nothing
0298 }
0299 
0300 void PlainText::setText(const QString &text)
0301 {
0302     m_text = text;
0303 }
0304 
0305 QString PlainText::text() const
0306 {
0307     return m_text;
0308 }
0309 
0310 void PlainText::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
0311 {
0312     if (replaceMode == ValueItem::ReplaceMode::AnySubstring)
0313         m_text = m_text.replace(before, after);
0314     else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch && m_text == before)
0315         m_text = after;
0316 }
0317 
0318 bool PlainText::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
0319 {
0320     const QString text = QString(m_text).remove(ignoredInSorting);
0321     return text.contains(pattern, caseSensitive);
0322 }
0323 
0324 bool PlainText::operator==(const ValueItem &other) const
0325 {
0326     const PlainText *otherPlainText = dynamic_cast<const PlainText *>(&other);
0327     if (otherPlainText != nullptr) {
0328         return otherPlainText->text() == text();
0329     } else
0330         return false;
0331 }
0332 
0333 bool PlainText::isPlainText(const ValueItem &other) {
0334     return typeid(other) == typeid(PlainText);
0335 }
0336 
0337 QDebug operator<<(QDebug dbg, const PlainText &plainText) {
0338     dbg.nospace() << "PlainText " << plainText.text();
0339     return dbg;
0340 }
0341 
0342 
0343 VerbatimText::VerbatimText(const VerbatimText &other)
0344         : m_hasComment(other.hasComment()), m_text(other.text()), m_comment(other.comment())
0345 {
0346     /// nothing
0347 }
0348 
0349 VerbatimText::VerbatimText(const QString &text)
0350         : m_hasComment(false), m_text(text)
0351 {
0352     /// nothing
0353 }
0354 
0355 void VerbatimText::setText(const QString &text)
0356 {
0357     m_text = text;
0358 }
0359 
0360 QString VerbatimText::text() const
0361 {
0362     return m_text;
0363 }
0364 
0365 bool VerbatimText::hasComment() const
0366 {
0367     return m_hasComment;
0368 }
0369 
0370 void VerbatimText::removeComment()
0371 {
0372     m_hasComment = false;
0373     m_comment.clear();
0374 }
0375 
0376 void VerbatimText::setComment(const QString &comment)
0377 {
0378     m_hasComment = true;
0379     m_comment = comment;
0380 }
0381 
0382 QString VerbatimText::comment() const
0383 {
0384     return m_comment;
0385 }
0386 
0387 void VerbatimText::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
0388 {
0389     if (replaceMode == ValueItem::ReplaceMode::AnySubstring) {
0390         m_text = m_text.replace(before, after);
0391         m_comment = m_comment.replace(before, after);
0392     } else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch) {
0393         if (m_text == before)
0394             m_text = after;
0395         if (m_hasComment && m_comment == before)
0396             m_comment = after;
0397     }
0398 }
0399 
0400 bool VerbatimText::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
0401 {
0402     const QString text = QString(m_text).remove(ignoredInSorting);
0403 
0404     bool contained = text.contains(pattern, caseSensitive);
0405     if (!contained) {
0406         /// Only if simple text match failed, check color labels
0407         /// For a match, the user's pattern has to be the start of the color label
0408         /// and this verbatim text has to contain the color as hex string
0409         for (QVector<QPair<QString, QString>>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); !contained && it != Preferences::instance().colorCodes().constEnd(); ++it)
0410             contained = text.compare(it->first, Qt::CaseInsensitive) == 0 && it->second.contains(pattern, Qt::CaseInsensitive);
0411     }
0412 
0413     if (!contained && m_hasComment) {
0414         const QString comment = QString(m_comment).remove(ignoredInSorting);
0415         contained = comment.contains(pattern, caseSensitive);
0416         /// Do not test for colors in comment
0417     }
0418 
0419     return contained;
0420 }
0421 
0422 bool VerbatimText::operator==(const ValueItem &other) const
0423 {
0424     const VerbatimText *otherVerbatimText = dynamic_cast<const VerbatimText *>(&other);
0425     if (otherVerbatimText != nullptr) {
0426         return otherVerbatimText->text() == text() && (!m_hasComment || otherVerbatimText->comment() == comment());
0427     } else
0428         return false;
0429 }
0430 
0431 bool VerbatimText::isVerbatimText(const ValueItem &other) {
0432     return typeid(other) == typeid(VerbatimText);
0433 }
0434 
0435 QDebug operator<<(QDebug dbg, const VerbatimText &verbatimText) {
0436     dbg.nospace() << "VerbatimText " << verbatimText.text() << " (" << (verbatimText.hasComment() ? "" : "NO ") << "comment: " << verbatimText.comment() << ")";
0437     return dbg;
0438 }
0439 
0440 
0441 Value::Value()
0442         : QVector<QSharedPointer<ValueItem> >()
0443 {
0444     /// nothing
0445 }
0446 
0447 Value::Value(const Value &other)
0448         : QVector<QSharedPointer<ValueItem> >(other)
0449 {
0450     /// nothing
0451 }
0452 
0453 Value::Value(Value &&other)
0454         : QVector<QSharedPointer<ValueItem> >(other)
0455 {
0456     /// nothing
0457 }
0458 
0459 Value::~Value()
0460 {
0461     clear();
0462 }
0463 
0464 void Value::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
0465 {
0466     QSet<QSharedPointer<ValueItem> > unique;
0467     /// Delegate the replace operation to each individual ValueItem
0468     /// contained in this Value object
0469     for (Value::Iterator it = begin(); it != end();) {
0470         (*it)->replace(before, after, replaceMode);
0471 
0472         bool containedInUnique = false;
0473         for (const auto &valueItem : const_cast<const QSet<QSharedPointer<ValueItem> > &>(unique)) {
0474             containedInUnique = *valueItem.data() == *(*it).data();
0475             if (containedInUnique) break;
0476         }
0477 
0478         if (containedInUnique)
0479             it = erase(it);
0480         else {
0481             unique.insert(*it);
0482             ++it;
0483         }
0484     }
0485 
0486     QSet<QString> uniqueValueItemTexts;
0487     for (int i = count() - 1; i >= 0; --i) {
0488         at(i)->replace(before, after, replaceMode);
0489         const QString valueItemText = PlainTextValue::text(*at(i).data());
0490         if (uniqueValueItemTexts.contains(valueItemText)) {
0491             /// Due to a replace/delete operation above, an old ValueItem's text
0492             /// matches the replaced text.
0493             /// Therefore, remove the replaced text to avoid duplicates
0494             remove(i);
0495             ++i; /// compensate for for-loop's --i
0496         } else
0497             uniqueValueItemTexts.insert(valueItemText);
0498     }
0499 }
0500 
0501 void Value::replace(const QString &before, const QSharedPointer<ValueItem> &after)
0502 {
0503     const QString valueText = PlainTextValue::text(*this);
0504     if (valueText == before) {
0505         clear();
0506         append(after);
0507     } else {
0508         QSet<QString> uniqueValueItemTexts;
0509         for (int i = count() - 1; i >= 0; --i) {
0510             QString valueItemText = PlainTextValue::text(*at(i).data());
0511             if (valueItemText == before) {
0512                 /// Perform replacement operation
0513                 QVector<QSharedPointer<ValueItem> >::replace(i, after);
0514                 valueItemText = PlainTextValue::text(*after.data());
0515                 //  uniqueValueItemTexts.insert(PlainTextValue::text(*after.data()));
0516             }
0517 
0518             if (uniqueValueItemTexts.contains(valueItemText)) {
0519                 /// Due to a previous replace operation, an existingValueItem's
0520                 /// text matches a text which was inserted as an "after" ValueItem.
0521                 /// Therefore, remove the old ValueItem to avoid duplicates.
0522                 remove(i);
0523             } else {
0524                 /// Neither a replacement, nor a duplicate. Keep this
0525                 /// ValueItem (memorize as unique) and continue.
0526                 uniqueValueItemTexts.insert(valueItemText);
0527             }
0528         }
0529     }
0530 }
0531 
0532 bool Value::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
0533 {
0534     for (const auto &valueItem : const_cast<const Value &>(*this)) {
0535         if (valueItem->containsPattern(pattern, caseSensitive))
0536             return true;
0537     }
0538     return false;
0539 }
0540 
0541 bool Value::contains(const ValueItem &item) const
0542 {
0543     for (const auto &valueItem : const_cast<const Value &>(*this))
0544         if (valueItem->operator==(item))
0545             return true;
0546     return false;
0547 }
0548 
0549 Value &Value::operator=(const Value &rhs)
0550 {
0551     return static_cast<Value &>(QVector<QSharedPointer<ValueItem> >::operator =((rhs)));
0552 }
0553 
0554 Value &Value::operator=(Value &&rhs)
0555 {
0556     return static_cast<Value &>(QVector<QSharedPointer<ValueItem> >::operator =((rhs)));
0557 }
0558 
0559 Value &Value::operator<<(const QSharedPointer<ValueItem> &value)
0560 {
0561     return static_cast<Value &>(QVector<QSharedPointer<ValueItem> >::operator<<((value)));
0562 }
0563 
0564 bool Value::operator==(const Value &rhs) const
0565 {
0566     const Value &lhs = *this; ///< just for readability to have a 'lhs' matching 'rhs'
0567 
0568     /// Obviously, both Values must be of same size
0569     if (lhs.count() != rhs.count()) return false;
0570 
0571     /// Synchronously iterate over both Values' ValueItems
0572     for (Value::ConstIterator lhsIt = lhs.constBegin(), rhsIt = rhs.constBegin(); lhsIt != lhs.constEnd() && rhsIt != rhs.constEnd(); ++lhsIt, ++rhsIt) {
0573         /// Are both ValueItems PlainTexts and are both PlainTexts equal?
0574         const QSharedPointer<PlainText> lhsPlainText = lhsIt->dynamicCast<PlainText>();
0575         const QSharedPointer<PlainText> rhsPlainText = rhsIt->dynamicCast<PlainText>();
0576         if ((lhsPlainText.isNull() && !rhsPlainText.isNull()) || (!lhsPlainText.isNull() && rhsPlainText.isNull())) return false;
0577         if (!lhsPlainText.isNull() && !rhsPlainText.isNull()) {
0578             if (*lhsPlainText.data() != *rhsPlainText.data())
0579                 return false;
0580         } else {
0581             /// Remainder of comparisons is like for PlainText above, just for other descendants of ValueItem
0582             const QSharedPointer<MacroKey> lhsMacroKey = lhsIt->dynamicCast<MacroKey>();
0583             const QSharedPointer<MacroKey> rhsMacroKey = rhsIt->dynamicCast<MacroKey>();
0584             if ((lhsMacroKey.isNull() && !rhsMacroKey.isNull()) || (!lhsMacroKey.isNull() && rhsMacroKey.isNull())) return false;
0585             if (!lhsMacroKey.isNull() && !rhsMacroKey.isNull()) {
0586                 if (*lhsMacroKey.data() != *rhsMacroKey.data())
0587                     return false;
0588             } else {
0589                 const QSharedPointer<Person> lhsPerson = lhsIt->dynamicCast<Person>();
0590                 const QSharedPointer<Person> rhsPerson = rhsIt->dynamicCast<Person>();
0591                 if ((lhsPerson.isNull() && !rhsPerson.isNull()) || (!lhsPerson.isNull() && rhsPerson.isNull())) return false;
0592                 if (!lhsPerson.isNull() && !rhsPerson.isNull()) {
0593                     if (*lhsPerson.data() != *rhsPerson.data())
0594                         return false;
0595                 } else {
0596                     const QSharedPointer<VerbatimText> lhsVerbatimText = lhsIt->dynamicCast<VerbatimText>();
0597                     const QSharedPointer<VerbatimText> rhsVerbatimText = rhsIt->dynamicCast<VerbatimText>();
0598                     if ((lhsVerbatimText.isNull() && !rhsVerbatimText.isNull()) || (!lhsVerbatimText.isNull() && rhsVerbatimText.isNull())) return false;
0599                     if (!lhsVerbatimText.isNull() && !rhsVerbatimText.isNull()) {
0600                         if (*lhsVerbatimText.data() != *rhsVerbatimText.data())
0601                             return false;
0602                     } else {
0603                         const QSharedPointer<Keyword> lhsKeyword = lhsIt->dynamicCast<Keyword>();
0604                         const QSharedPointer<Keyword> rhsKeyword = rhsIt->dynamicCast<Keyword>();
0605                         if ((lhsKeyword.isNull() && !rhsKeyword.isNull()) || (!lhsKeyword.isNull() && rhsKeyword.isNull())) return false;
0606                         if (!lhsKeyword.isNull() && !rhsKeyword.isNull()) {
0607                             if (*lhsKeyword.data() != *rhsKeyword.data())
0608                                 return false;
0609                         } else {
0610                             /// If there are other descendants of ValueItem, add tests here ...
0611                             return false;
0612                         }
0613                     }
0614                 }
0615             }
0616         }
0617     }
0618 
0619     /// No check failed, so equalness is proven
0620     return true;
0621 }
0622 
0623 bool Value::operator!=(const Value &rhs) const
0624 {
0625     return !operator ==(rhs);
0626 }
0627 
0628 QDebug operator<<(QDebug dbg, const Value &value) {
0629     dbg.nospace() << "Value";
0630     if (value.isEmpty())
0631         dbg << " is empty";
0632     else
0633         dbg.nospace() << ": " << PlainTextValue::text(value);
0634     return dbg;
0635 }
0636 
0637 
0638 QString PlainTextValue::text(const Value &value, const FormattingOptions formattingOptions)
0639 {
0640     ValueItemType vit = ValueItemType::Other;
0641     ValueItemType lastVit = ValueItemType::Other;
0642 
0643     if (formattingOptions.testFlag(FormattingOption::BeautifyMonth) && value.length() >= 1 && value.length() <= 3) {
0644         int firstIndex = -1, lastIndex = -1;
0645         if (MacroKey::isMacroKey(*value.first()) && MacroKey::isMacroKey(*value.last())) {
0646             // Probably one macro key like  jan  or a triple like  jan "#" feb   or just  jan feb
0647             const QString firstText = value.first().dynamicCast<MacroKey>()->text().toLower();
0648             const QString lastText = value.length() > 1 ? value.last().dynamicCast<MacroKey>()->text().toLower() : QString();
0649             firstIndex = std::distance(KBibTeX::MonthsTriple, std::find(KBibTeX::MonthsTriple, KBibTeX::MonthsTriple + 12, firstText)) + 1;
0650             lastIndex = value.length() > 1 ? std::distance(KBibTeX::MonthsTriple, std::find(KBibTeX::MonthsTriple, KBibTeX::MonthsTriple + 12, lastText)) + 1 : firstIndex;
0651         } else {
0652             // Some text that may contain a number like  5
0653             bool ok = false;
0654             firstIndex = PlainTextValue::text(value.first()).toInt(&ok);
0655             if (!ok || (firstIndex < 1) || (firstIndex > 12))
0656                 firstIndex = -1;
0657             ok = true;
0658             lastIndex = value.length() > 1 ? PlainTextValue::text(value.last()).toInt(&ok) : firstIndex;
0659             if (!ok || (lastIndex < 1) || (lastIndex > 12))
0660                 lastIndex = -1;
0661             static const QSet<QString> centerOfThree{QStringLiteral("#"), QStringLiteral("-"), QStringLiteral("--"), QStringLiteral("/"), QChar(0x2013)};
0662             if (value.length() == 3 && !centerOfThree.contains(PlainTextValue::text(value.at(1)))) {
0663                 // If value contains three elements, the center one must be one of the allowed strings in the set
0664                 lastIndex = firstIndex = -1;
0665             }
0666         }
0667         if (firstIndex >= 1 && lastIndex <= 12 && lastIndex >= 1 && lastIndex <= 12) {
0668             if (value.length() == 1) {
0669                 // Detour via format string necessary due to bug QT-113415
0670                 return QString(QStringLiteral("%1")).arg(QLocale::system().monthName(firstIndex));
0671             }
0672             else
0673                 return QString(QStringLiteral("%1%2%3")).arg(QLocale::system().monthName(firstIndex), QChar(0x2013), QLocale::system().monthName(lastIndex));
0674         } else {
0675             qCDebug(LOG_KBIBTEX_DATA) << "Got asked to beautify month, but could not determine index";
0676         }
0677     }
0678 
0679     QString result;
0680     for (const auto &valueItem : value) {
0681         QString nextText = text(*valueItem, vit);
0682         if (!nextText.isEmpty()) {
0683             if (lastVit == ValueItemType::Person && vit == ValueItemType::Person)
0684                 result.append(i18n(" and ")); // TODO proper list of authors/editors, not just joined by "and"
0685             else if (lastVit == ValueItemType::Person && vit == ValueItemType::Other && nextText == QStringLiteral("others")) {
0686                 /// "and others" case: replace text to be appended by translated variant
0687                 nextText = i18n(" and others");
0688             } else if (lastVit == ValueItemType::Keyword && vit == ValueItemType::Keyword)
0689                 result.append(QStringLiteral("; "));
0690             else if (!result.isEmpty())
0691                 result.append(QStringLiteral(" "));
0692             result.append(nextText);
0693 
0694             lastVit = vit;
0695         }
0696     }
0697     return result;
0698 }
0699 
0700 QString PlainTextValue::text(const QSharedPointer<const ValueItem> &valueItem)
0701 {
0702     const ValueItem *p = valueItem.data();
0703     return text(*p);
0704 }
0705 
0706 QString PlainTextValue::text(const ValueItem &valueItem)
0707 {
0708     ValueItemType vit;
0709     return text(valueItem, vit);
0710 }
0711 
0712 QString PlainTextValue::text(const ValueItem &valueItem, ValueItemType &vit)
0713 {
0714     QString result;
0715     vit = ValueItemType::Other;
0716 
0717     bool isVerbatim = false;
0718     const PlainText *plainText = dynamic_cast<const PlainText *>(&valueItem);
0719     if (plainText != nullptr) {
0720         result = plainText->text();
0721     } else {
0722         const MacroKey *macroKey = dynamic_cast<const MacroKey *>(&valueItem);
0723         if (macroKey != nullptr) {
0724             result = macroKey->text(); // TODO Use File to resolve key to full text
0725         } else {
0726             const Person *person = dynamic_cast<const Person *>(&valueItem);
0727             if (person != nullptr) {
0728                 result = Person::transcribePersonName(person, Preferences::instance().personNameFormat());
0729                 vit = ValueItemType::Person;
0730             } else {
0731                 const Keyword *keyword = dynamic_cast<const Keyword *>(&valueItem);
0732                 if (keyword != nullptr) {
0733                     result = keyword->text();
0734                     vit = ValueItemType::Keyword;
0735                 } else {
0736                     const VerbatimText *verbatimText = dynamic_cast<const VerbatimText *>(&valueItem);
0737                     if (verbatimText != nullptr) {
0738                         result = verbatimText->text();
0739                         isVerbatim = true;
0740                     } else
0741                         qCWarning(LOG_KBIBTEX_DATA) << "Cannot interpret ValueItem to one of its descendants";
0742                 }
0743             }
0744         }
0745     }
0746 
0747     /// clean up result string
0748     const int len = result.length();
0749     int j = 0;
0750     static const QChar cbo = QLatin1Char('{'), cbc = QLatin1Char('}'), bs = QLatin1Char('\\'), mns = QLatin1Char('-'), comma = QLatin1Char(','), thinspace = QChar(0x2009), tilde = QLatin1Char('~'), nobreakspace = QChar(0x00a0);
0751     for (int i = 0; i < len; ++i) {
0752         if ((result[i] == cbo || result[i] == cbc) && (i < 1 || result[i - 1] != bs)) {
0753             /// hop over curly brackets
0754         } else if (i < len - 1 && result[i] == bs && result[i + 1] == mns) {
0755             /// hop over hyphenation commands
0756             ++i;
0757         } else if (i < len - 1 && result[i] == bs && result[i + 1] == comma) {
0758             /// place '\,' with a thin space
0759             result[j] = thinspace;
0760             ++i; ++j;
0761         } else if (!isVerbatim && result[i] == tilde && (i < 1 || result[i - 1] != bs))  {
0762             /// replace '~' with a non-breaking space,
0763             /// except if text was verbatim (e.g. a 'localfile' value
0764             /// like '~/document.pdf' or 'document.pdf~')
0765             result[j] = nobreakspace;
0766             ++j;
0767         } else {
0768             if (i > j) {
0769                 /// move individual characters forward in result string
0770                 result[j] = result[i];
0771             }
0772             ++j;
0773         }
0774     }
0775     result.resize(j);
0776 
0777     return result;
0778 }