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 ¯okey) { 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 }