1 /***************************************************************************
2  *   Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
16  ***************************************************************************/
17 
18 #include "value.h"
19 
20 #include <typeinfo>
21 
22 #include <QSet>
23 #include <QString>
24 #include <QStringList>
25 #include <QDebug>
26 #include <QRegularExpression>
27 
28 #ifdef HAVE_KF5
29 #include <KSharedConfig>
30 #include <KConfigGroup>
31 #include <KLocalizedString>
32 #else // HAVE_KF5
33 #define i18n(text) QObject::tr(text)
34 #endif // HAVE_KF5
35 
36 #include "file.h"
37 #include "preferences.h"
38 
39 quint64 ValueItem::internalIdCounter = 0;
40 
qHash(const QSharedPointer<ValueItem> & valueItem)41 uint qHash(const QSharedPointer<ValueItem> &valueItem)
42 {
43     return qHash(valueItem->id());
44 }
45 
46 const QRegularExpression ValueItem::ignoredInSorting(QStringLiteral("[{}\\\\]+"));
47 
ValueItem()48 ValueItem::ValueItem()
49         : internalId(++internalIdCounter)
50 {
51     /// nothing
52 }
53 
~ValueItem()54 ValueItem::~ValueItem()
55 {
56     /// nothing
57 }
58 
id() const59 quint64 ValueItem::id() const
60 {
61     return internalId;
62 }
63 
operator !=(const ValueItem & other) const64 bool ValueItem::operator!=(const ValueItem &other) const
65 {
66     return !operator ==(other);
67 }
68 
Keyword(const Keyword & other)69 Keyword::Keyword(const Keyword &other)
70         : m_text(other.m_text)
71 {
72     /// nothing
73 }
74 
Keyword(const QString & text)75 Keyword::Keyword(const QString &text)
76         : m_text(text)
77 {
78     /// nothing
79 }
80 
setText(const QString & text)81 void Keyword::setText(const QString &text)
82 {
83     m_text = text;
84 }
85 
text() const86 QString Keyword::text() const
87 {
88     return m_text;
89 }
90 
replace(const QString & before,const QString & after,ValueItem::ReplaceMode replaceMode)91 void Keyword::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
92 {
93     if (replaceMode == ValueItem::AnySubstring)
94         m_text = m_text.replace(before, after);
95     else if (replaceMode == ValueItem::CompleteMatch && m_text == before)
96         m_text = after;
97 }
98 
containsPattern(const QString & pattern,Qt::CaseSensitivity caseSensitive) const99 bool Keyword::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
100 {
101     const QString text = QString(m_text).remove(ignoredInSorting);
102     return text.contains(pattern, caseSensitive);
103 }
104 
operator ==(const ValueItem & other) const105 bool Keyword::operator==(const ValueItem &other) const
106 {
107     const Keyword *otherKeyword = dynamic_cast<const Keyword *>(&other);
108     if (otherKeyword != nullptr) {
109         return otherKeyword->text() == text();
110     } else
111         return false;
112 }
113 
isKeyword(const ValueItem & other)114 bool Keyword::isKeyword(const ValueItem &other) {
115     return typeid(other) == typeid(Keyword);
116 }
117 
118 
Person(const QString & firstName,const QString & lastName,const QString & suffix)119 Person::Person(const QString &firstName, const QString &lastName, const QString &suffix)
120         : m_firstName(firstName), m_lastName(lastName), m_suffix(suffix)
121 {
122     /// nothing
123 }
124 
Person(const Person & other)125 Person::Person(const Person &other)
126         : m_firstName(other.firstName()), m_lastName(other.lastName()), m_suffix(other.suffix())
127 {
128     /// nothing
129 }
130 
firstName() const131 QString Person::firstName() const
132 {
133     return m_firstName;
134 }
135 
lastName() const136 QString Person::lastName() const
137 {
138     return m_lastName;
139 }
140 
suffix() const141 QString Person::suffix() const
142 {
143     return m_suffix;
144 }
145 
replace(const QString & before,const QString & after,ValueItem::ReplaceMode replaceMode)146 void Person::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
147 {
148     if (replaceMode == ValueItem::AnySubstring) {
149         m_firstName = m_firstName.replace(before, after);
150         m_lastName = m_lastName.replace(before, after);
151         m_suffix = m_suffix.replace(before, after);
152     } else if (replaceMode == ValueItem::CompleteMatch) {
153         if (m_firstName == before)
154             m_firstName = after;
155         if (m_lastName == before)
156             m_lastName = after;
157         if (m_suffix == before)
158             m_suffix = after;
159     }
160 }
161 
containsPattern(const QString & pattern,Qt::CaseSensitivity caseSensitive) const162 bool Person::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
163 {
164     const QString firstName = QString(m_firstName).remove(ignoredInSorting);
165     const QString lastName = QString(m_lastName).remove(ignoredInSorting);
166     const QString suffix = QString(m_suffix).remove(ignoredInSorting);
167 
168     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);
169 }
170 
operator ==(const ValueItem & other) const171 bool Person::operator==(const ValueItem &other) const
172 {
173     const Person *otherPerson = dynamic_cast<const Person *>(&other);
174     if (otherPerson != nullptr) {
175         return otherPerson->firstName() == firstName() && otherPerson->lastName() == lastName() && otherPerson->suffix() == suffix();
176     } else
177         return false;
178 }
179 
transcribePersonName(const Person * person,const QString & formatting)180 QString Person::transcribePersonName(const Person *person, const QString &formatting)
181 {
182     return transcribePersonName(formatting, person->firstName(), person->lastName(), person->suffix());
183 }
184 
transcribePersonName(const QString & formatting,const QString & firstName,const QString & lastName,const QString & suffix)185 QString Person::transcribePersonName(const QString &formatting, const QString &firstName, const QString &lastName, const QString &suffix)
186 {
187     QString result = formatting;
188     int p1 = -1, p2 = -1, p3 = -1;
189     while ((p1 = result.indexOf('<')) >= 0 && (p2 = result.indexOf('>', p1 + 1)) >= 0 && (p3 = result.indexOf('%', p1)) >= 0 && p3 < p2) {
190         QString insert;
191         switch (result[p3 + 1].toLatin1()) {
192         case 'f':
193             insert = firstName;
194             break;
195         case 'l':
196             insert = lastName;
197             break;
198         case 's':
199             insert = suffix;
200             break;
201         }
202 
203         if (!insert.isEmpty())
204             insert = result.mid(p1 + 1, p3 - p1 - 1) + insert + result.mid(p3 + 2, p2 - p3 - 2);
205 
206         result = result.left(p1) + insert + result.mid(p2 + 1);
207     }
208     return result;
209 }
210 
isPerson(const ValueItem & other)211 bool Person::isPerson(const ValueItem &other) {
212     return typeid(other) == typeid(Person);
213 }
214 
operator <<(QDebug dbg,const Person & person)215 QDebug operator<<(QDebug dbg, const Person &person) {
216     dbg.nospace() << "Person " << Person::transcribePersonName(&person, Preferences::defaultPersonNameFormatting);
217     return dbg;
218 }
219 
220 
MacroKey(const MacroKey & other)221 MacroKey::MacroKey(const MacroKey &other)
222         : m_text(other.m_text)
223 {
224     /// nothing
225 }
226 
MacroKey(const QString & text)227 MacroKey::MacroKey(const QString &text)
228         : m_text(text)
229 {
230     /// nothing
231 }
232 
setText(const QString & text)233 void MacroKey::setText(const QString &text)
234 {
235     m_text = text;
236 }
237 
text() const238 QString MacroKey::text() const
239 {
240     return m_text;
241 }
242 
isValid()243 bool MacroKey::isValid()
244 {
245     const QString t = text();
246     static const QRegularExpression validMacroKey(QStringLiteral("^[a-z][-.:/+_a-z0-9]*$|^[0-9]+$"), QRegularExpression::CaseInsensitiveOption);
247     const QRegularExpressionMatch match = validMacroKey.match(t);
248     return match.hasMatch() && match.captured(0) == t;
249 }
250 
replace(const QString & before,const QString & after,ValueItem::ReplaceMode replaceMode)251 void MacroKey::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
252 {
253     if (replaceMode == ValueItem::AnySubstring)
254         m_text = m_text.replace(before, after);
255     else if (replaceMode == ValueItem::CompleteMatch && m_text == before)
256         m_text = after;
257 }
258 
containsPattern(const QString & pattern,Qt::CaseSensitivity caseSensitive) const259 bool MacroKey::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
260 {
261     const QString text = QString(m_text).remove(ignoredInSorting);
262     return text.contains(pattern, caseSensitive);
263 }
264 
operator ==(const ValueItem & other) const265 bool MacroKey::operator==(const ValueItem &other) const
266 {
267     const MacroKey *otherMacroKey = dynamic_cast<const MacroKey *>(&other);
268     if (otherMacroKey != nullptr) {
269         return otherMacroKey->text() == text();
270     } else
271         return false;
272 }
273 
isMacroKey(const ValueItem & other)274 bool MacroKey::isMacroKey(const ValueItem &other) {
275     return typeid(other) == typeid(MacroKey);
276 }
277 
operator <<(QDebug dbg,const MacroKey & macrokey)278 QDebug operator<<(QDebug dbg, const MacroKey &macrokey) {
279     dbg.nospace() << "MacroKey " << macrokey.text();
280     return dbg;
281 }
282 
283 
PlainText(const PlainText & other)284 PlainText::PlainText(const PlainText &other)
285         : m_text(other.text())
286 {
287     /// nothing
288 }
289 
PlainText(const QString & text)290 PlainText::PlainText(const QString &text)
291         : m_text(text)
292 {
293     /// nothing
294 }
295 
setText(const QString & text)296 void PlainText::setText(const QString &text)
297 {
298     m_text = text;
299 }
300 
text() const301 QString PlainText::text() const
302 {
303     return m_text;
304 }
305 
replace(const QString & before,const QString & after,ValueItem::ReplaceMode replaceMode)306 void PlainText::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
307 {
308     if (replaceMode == ValueItem::AnySubstring)
309         m_text = m_text.replace(before, after);
310     else if (replaceMode == ValueItem::CompleteMatch && m_text == before)
311         m_text = after;
312 }
313 
containsPattern(const QString & pattern,Qt::CaseSensitivity caseSensitive) const314 bool PlainText::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
315 {
316     const QString text = QString(m_text).remove(ignoredInSorting);
317     return text.contains(pattern, caseSensitive);
318 }
319 
operator ==(const ValueItem & other) const320 bool PlainText::operator==(const ValueItem &other) const
321 {
322     const PlainText *otherPlainText = dynamic_cast<const PlainText *>(&other);
323     if (otherPlainText != nullptr) {
324         return otherPlainText->text() == text();
325     } else
326         return false;
327 }
328 
isPlainText(const ValueItem & other)329 bool PlainText::isPlainText(const ValueItem &other) {
330     return typeid(other) == typeid(PlainText);
331 }
332 
operator <<(QDebug dbg,const PlainText & plainText)333 QDebug operator<<(QDebug dbg, const PlainText &plainText) {
334     dbg.nospace() << "PlainText " << plainText.text();
335     return dbg;
336 }
337 
338 
339 #ifdef HAVE_KF5
340 bool VerbatimText::colorLabelPairsInitialized = false;
341 QList<VerbatimText::ColorLabelPair> VerbatimText::colorLabelPairs = QList<VerbatimText::ColorLabelPair>();
342 #endif // HAVE_KF5
343 
VerbatimText(const VerbatimText & other)344 VerbatimText::VerbatimText(const VerbatimText &other)
345         : m_text(other.text())
346 {
347     /// nothing
348 }
349 
VerbatimText(const QString & text)350 VerbatimText::VerbatimText(const QString &text)
351         : m_text(text)
352 {
353     /// nothing
354 }
355 
setText(const QString & text)356 void VerbatimText::setText(const QString &text)
357 {
358     m_text = text;
359 }
360 
text() const361 QString VerbatimText::text() const
362 {
363     return m_text;
364 }
365 
replace(const QString & before,const QString & after,ValueItem::ReplaceMode replaceMode)366 void VerbatimText::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
367 {
368     if (replaceMode == ValueItem::AnySubstring)
369         m_text = m_text.replace(before, after);
370     else if (replaceMode == ValueItem::CompleteMatch && m_text == before)
371         m_text = after;
372 }
373 
containsPattern(const QString & pattern,Qt::CaseSensitivity caseSensitive) const374 bool VerbatimText::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
375 {
376     const QString text = QString(m_text).remove(ignoredInSorting);
377 
378 #ifdef HAVE_KF5
379     /// Initialize map of labels to color (hex string) only once
380     // FIXME if user changes colors/labels later, it will not be updated here
381     if (!colorLabelPairsInitialized) {
382         colorLabelPairsInitialized = true;
383 
384         /// Read data from config file
385         KSharedConfigPtr config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc")));
386         KConfigGroup configGroup(config, Preferences::groupColor);
387         const QStringList colorCodes = configGroup.readEntry(Preferences::keyColorCodes, Preferences::defaultColorCodes);
388         const QStringList colorLabels = configGroup.readEntry(Preferences::keyColorLabels, Preferences::defaultColorLabels);
389 
390         /// Translate data from config file into internal mapping
391         for (QStringList::ConstIterator itc = colorCodes.constBegin(), itl = colorLabels.constBegin(); itc != colorCodes.constEnd() && itl != colorLabels.constEnd(); ++itc, ++itl) {
392             ColorLabelPair clp;
393             clp.hexColor = *itc;
394             clp.label = i18n((*itl).toUtf8().constData());
395             colorLabelPairs << clp;
396         }
397     }
398 #endif //  HAVE_KF5
399 
400     bool contained = text.contains(pattern, caseSensitive);
401 #ifdef HAVE_KF5
402     if (!contained) {
403         /// Only if simple text match failed, check color labels
404         /// For a match, the user's pattern has to be the start of the color label
405         /// and this verbatim text has to contain the color as hex string
406         for (const auto &clp : const_cast<const QList<ColorLabelPair> &>(colorLabelPairs)) {
407             contained = text.compare(clp.hexColor, Qt::CaseInsensitive) == 0 && clp.label.contains(pattern, Qt::CaseInsensitive);
408             if (contained) break;
409         }
410     }
411 #endif // HAVE_KF5
412 
413     return contained;
414 }
415 
operator ==(const ValueItem & other) const416 bool VerbatimText::operator==(const ValueItem &other) const
417 {
418     const VerbatimText *otherVerbatimText = dynamic_cast<const VerbatimText *>(&other);
419     if (otherVerbatimText != nullptr) {
420         return otherVerbatimText->text() == text();
421     } else
422         return false;
423 }
424 
isVerbatimText(const ValueItem & other)425 bool VerbatimText::isVerbatimText(const ValueItem &other) {
426     return typeid(other) == typeid(VerbatimText);
427 }
428 
operator <<(QDebug dbg,const VerbatimText & verbatimText)429 QDebug operator<<(QDebug dbg, const VerbatimText &verbatimText) {
430     dbg.nospace() << "VerbatimText " << verbatimText.text();
431     return dbg;
432 }
433 
434 
Value()435 Value::Value()
436         : QVector<QSharedPointer<ValueItem> >()
437 {
438     /// nothing
439 }
440 
Value(const Value & other)441 Value::Value(const Value &other)
442         : QVector<QSharedPointer<ValueItem> >(other)
443 {
444     /// nothing
445 }
446 
Value(Value && other)447 Value::Value(Value &&other)
448         : QVector<QSharedPointer<ValueItem> >(other)
449 {
450     /// nothing
451 }
452 
~Value()453 Value::~Value()
454 {
455     clear();
456 }
457 
replace(const QString & before,const QString & after,ValueItem::ReplaceMode replaceMode)458 void Value::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode)
459 {
460     QSet<QSharedPointer<ValueItem> > unique;
461     /// Delegate the replace operation to each individual ValueItem
462     /// contained in this Value object
463     for (Value::Iterator it = begin(); it != end();) {
464         (*it)->replace(before, after, replaceMode);
465 
466         bool containedInUnique = false;
467         for (const auto &valueItem : const_cast<const QSet<QSharedPointer<ValueItem> > &>(unique)) {
468             containedInUnique = *valueItem.data() == *(*it).data();
469             if (containedInUnique) break;
470         }
471 
472         if (containedInUnique)
473             it = erase(it);
474         else {
475             unique.insert(*it);
476             ++it;
477         }
478     }
479 
480     QSet<QString> uniqueValueItemTexts;
481     for (int i = count() - 1; i >= 0; --i) {
482         at(i)->replace(before, after, replaceMode);
483         const QString valueItemText = PlainTextValue::text(*at(i).data());
484         if (uniqueValueItemTexts.contains(valueItemText)) {
485             /// Due to a replace/delete operation above, an old ValueItem's text
486             /// matches the replaced text.
487             /// Therefore, remove the replaced text to avoid duplicates
488             remove(i);
489             ++i; /// compensate for for-loop's --i
490         } else
491             uniqueValueItemTexts.insert(valueItemText);
492     }
493 }
494 
replace(const QString & before,const QSharedPointer<ValueItem> & after)495 void Value::replace(const QString &before, const QSharedPointer<ValueItem> &after)
496 {
497     const QString valueText = PlainTextValue::text(*this);
498     if (valueText == before) {
499         clear();
500         append(after);
501     } else {
502         QSet<QString> uniqueValueItemTexts;
503         for (int i = count() - 1; i >= 0; --i) {
504             QString valueItemText = PlainTextValue::text(*at(i).data());
505             if (valueItemText == before) {
506                 /// Perform replacement operation
507                 QVector<QSharedPointer<ValueItem> >::replace(i, after);
508                 valueItemText = PlainTextValue::text(*after.data());
509                 //  uniqueValueItemTexts.insert(PlainTextValue::text(*after.data()));
510             }
511 
512             if (uniqueValueItemTexts.contains(valueItemText)) {
513                 /// Due to a previous replace operation, an existingValueItem's
514                 /// text matches a text which was inserted as an "after" ValueItem.
515                 /// Therefore, remove the old ValueItem to avoid duplicates.
516                 remove(i);
517             } else {
518                 /// Neither a replacement, nor a duplicate. Keep this
519                 /// ValueItem (memorize as unique) and continue.
520                 uniqueValueItemTexts.insert(valueItemText);
521             }
522         }
523     }
524 }
525 
containsPattern(const QString & pattern,Qt::CaseSensitivity caseSensitive) const526 bool Value::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const
527 {
528     for (const auto &valueItem : const_cast<const Value &>(*this)) {
529         if (valueItem->containsPattern(pattern, caseSensitive))
530             return true;
531     }
532     return false;
533 }
534 
contains(const ValueItem & item) const535 bool Value::contains(const ValueItem &item) const
536 {
537     for (const auto &valueItem : const_cast<const Value &>(*this))
538         if (valueItem->operator==(item))
539             return true;
540     return false;
541 }
542 
operator =(const Value & rhs)543 Value &Value::operator=(const Value &rhs)
544 {
545     return static_cast<Value &>(QVector<QSharedPointer<ValueItem> >::operator =((rhs)));
546 }
547 
operator =(Value && rhs)548 Value &Value::operator=(Value &&rhs)
549 {
550     return static_cast<Value &>(QVector<QSharedPointer<ValueItem> >::operator =((rhs)));
551 }
552 
operator <<(const QSharedPointer<ValueItem> & value)553 Value &Value::operator<<(const QSharedPointer<ValueItem> &value)
554 {
555     return static_cast<Value &>(QVector<QSharedPointer<ValueItem> >::operator<<((value)));
556 }
557 
operator ==(const Value & rhs) const558 bool Value::operator==(const Value &rhs) const
559 {
560     const Value &lhs = *this; ///< just for readability to have a 'lhs' matching 'rhs'
561 
562     /// Obviously, both Values must be of same size
563     if (lhs.count() != rhs.count()) return false;
564 
565     /// Synchronously iterate over both Values' ValueItems
566     for (Value::ConstIterator lhsIt = lhs.constBegin(), rhsIt = rhs.constBegin(); lhsIt != lhs.constEnd() && rhsIt != rhs.constEnd(); ++lhsIt, ++rhsIt) {
567         /// Are both ValueItems PlainTexts and are both PlainTexts equal?
568         const QSharedPointer<PlainText> lhsPlainText = lhsIt->dynamicCast<PlainText>();
569         const QSharedPointer<PlainText> rhsPlainText = rhsIt->dynamicCast<PlainText>();
570         if ((lhsPlainText.isNull() && !rhsPlainText.isNull()) || (!lhsPlainText.isNull() && rhsPlainText.isNull())) return false;
571         if (!lhsPlainText.isNull() && !rhsPlainText.isNull()) {
572             if (*lhsPlainText.data() != *rhsPlainText.data())
573                 return false;
574         } else {
575             /// Remainder of comparisons is like for PlainText above, just for other descendants of ValueItem
576             const QSharedPointer<MacroKey> lhsMacroKey = lhsIt->dynamicCast<MacroKey>();
577             const QSharedPointer<MacroKey> rhsMacroKey = rhsIt->dynamicCast<MacroKey>();
578             if ((lhsMacroKey.isNull() && !rhsMacroKey.isNull()) || (!lhsMacroKey.isNull() && rhsMacroKey.isNull())) return false;
579             if (!lhsMacroKey.isNull() && !rhsMacroKey.isNull()) {
580                 if (*lhsMacroKey.data() != *rhsMacroKey.data())
581                     return false;
582             } else {
583                 const QSharedPointer<Person> lhsPerson = lhsIt->dynamicCast<Person>();
584                 const QSharedPointer<Person> rhsPerson = rhsIt->dynamicCast<Person>();
585                 if ((lhsPerson.isNull() && !rhsPerson.isNull()) || (!lhsPerson.isNull() && rhsPerson.isNull())) return false;
586                 if (!lhsPerson.isNull() && !rhsPerson.isNull()) {
587                     if (*lhsPerson.data() != *rhsPerson.data())
588                         return false;
589                 } else {
590                     const QSharedPointer<VerbatimText> lhsVerbatimText = lhsIt->dynamicCast<VerbatimText>();
591                     const QSharedPointer<VerbatimText> rhsVerbatimText = rhsIt->dynamicCast<VerbatimText>();
592                     if ((lhsVerbatimText.isNull() && !rhsVerbatimText.isNull()) || (!lhsVerbatimText.isNull() && rhsVerbatimText.isNull())) return false;
593                     if (!lhsVerbatimText.isNull() && !rhsVerbatimText.isNull()) {
594                         if (*lhsVerbatimText.data() != *rhsVerbatimText.data())
595                             return false;
596                     } else {
597                         const QSharedPointer<Keyword> lhsKeyword = lhsIt->dynamicCast<Keyword>();
598                         const QSharedPointer<Keyword> rhsKeyword = rhsIt->dynamicCast<Keyword>();
599                         if ((lhsKeyword.isNull() && !rhsKeyword.isNull()) || (!lhsKeyword.isNull() && rhsKeyword.isNull())) return false;
600                         if (!lhsKeyword.isNull() && !rhsKeyword.isNull()) {
601                             if (*lhsKeyword.data() != *rhsKeyword.data())
602                                 return false;
603                         } else {
604                             /// If there are other descendants of ValueItem, add tests here ...
605                             return false;
606                         }
607                     }
608                 }
609             }
610         }
611     }
612 
613     /// No check failed, so equalness is proven
614     return true;
615 }
616 
operator !=(const Value & rhs) const617 bool Value::operator!=(const Value &rhs) const
618 {
619     return !operator ==(rhs);
620 }
621 
operator <<(QDebug dbg,const Value & value)622 QDebug operator<<(QDebug dbg, const Value &value) {
623     dbg.nospace() << "Value";
624     if (value.isEmpty())
625         dbg << " is empty";
626     else
627         dbg.nospace() << ": " << PlainTextValue::text(value);
628     return dbg;
629 }
630 
631 
text(const Value & value)632 QString PlainTextValue::text(const Value &value)
633 {
634     ValueItemType vit = VITOther;
635     ValueItemType lastVit = VITOther;
636 
637     QString result;
638     for (const auto &valueItem : value) {
639         QString nextText = text(*valueItem, vit);
640         if (!nextText.isEmpty()) {
641             if (lastVit == VITPerson && vit == VITPerson)
642                 result.append(i18n(" and ")); // TODO proper list of authors/editors, not just joined by "and"
643             else if (lastVit == VITPerson && vit == VITOther && nextText == QStringLiteral("others")) {
644                 /// "and others" case: replace text to be appended by translated variant
645                 nextText = i18n(" and others");
646             } else if (lastVit == VITKeyword && vit == VITKeyword)
647                 result.append("; ");
648             else if (!result.isEmpty())
649                 result.append(" ");
650             result.append(nextText);
651 
652             lastVit = vit;
653         }
654     }
655     return result;
656 }
657 
text(const QSharedPointer<const ValueItem> & valueItem)658 QString PlainTextValue::text(const QSharedPointer<const ValueItem> &valueItem)
659 {
660     const ValueItem *p = valueItem.data();
661     return text(*p);
662 }
663 
text(const ValueItem & valueItem)664 QString PlainTextValue::text(const ValueItem &valueItem)
665 {
666     ValueItemType vit;
667     return text(valueItem, vit);
668 }
669 
text(const ValueItem & valueItem,ValueItemType & vit)670 QString PlainTextValue::text(const ValueItem &valueItem, ValueItemType &vit)
671 {
672     QString result;
673     vit = VITOther;
674 
675 #ifdef HAVE_KF5
676     /// Required to have static instance of PlainTextValue here
677     /// to initialize @see personNameFormatting from settings
678     /// as well as update @see personNameFormatting upon notification
679     /// from NotificationHub
680     static PlainTextValue ptv;
681 #endif // HAVE_KF5
682 
683     bool isVerbatim = false;
684     const PlainText *plainText = dynamic_cast<const PlainText *>(&valueItem);
685     if (plainText != nullptr) {
686         result = plainText->text();
687     } else {
688         const MacroKey *macroKey = dynamic_cast<const MacroKey *>(&valueItem);
689         if (macroKey != nullptr) {
690             result = macroKey->text(); // TODO Use File to resolve key to full text
691         } else {
692             const Person *person = dynamic_cast<const Person *>(&valueItem);
693             if (person != nullptr) {
694                 result = Person::transcribePersonName(person, personNameFormatting);
695                 vit = VITPerson;
696             } else {
697                 const Keyword *keyword = dynamic_cast<const Keyword *>(&valueItem);
698                 if (keyword != nullptr) {
699                     result = keyword->text();
700                     vit = VITKeyword;
701                 } else {
702                     const VerbatimText *verbatimText = dynamic_cast<const VerbatimText *>(&valueItem);
703                     if (verbatimText != nullptr) {
704                         result = verbatimText->text();
705                         isVerbatim = true;
706                     } else
707                         qWarning() << "Cannot interpret ValueItem to one of its descendants";
708                 }
709             }
710         }
711     }
712 
713     /// clean up result string
714     const int len = result.length();
715     int j = 0;
716     static const QChar cbo = QLatin1Char('{'), cbc = QLatin1Char('}'), bs = QLatin1Char('\\'), mns = QLatin1Char('-'), comma = QLatin1Char(','), thinspace = QChar(0x2009), tilde = QLatin1Char('~'), nobreakspace = QChar(0x00a0);
717     for (int i = 0; i < len; ++i) {
718         if ((result[i] == cbo || result[i] == cbc) && (i < 1 || result[i - 1] != bs)) {
719             /// hop over curly brackets
720         } else if (i < len - 1 && result[i] == bs && result[i + 1] == mns) {
721             /// hop over hyphenation commands
722             ++i;
723         } else if (i < len - 1 && result[i] == bs && result[i + 1] == comma) {
724             /// place '\,' with a thin space
725             result[j] = thinspace;
726             ++i; ++j;
727         } else if (!isVerbatim && result[i] == tilde && (i < 1 || result[i - 1] != bs))  {
728             /// replace '~' with a non-breaking space,
729             /// except if text was verbatim (e.g. a 'localfile' value
730             /// like '~/document.pdf' or 'document.pdf~')
731             result[j] = nobreakspace;
732             ++j;
733         } else {
734             if (i > j) {
735                 /// move individual characters forward in result string
736                 result[j] = result[i];
737             }
738             ++j;
739         }
740     }
741     result.resize(j);
742 
743     return result;
744 }
745 
746 #ifdef HAVE_KF5
PlainTextValue()747 PlainTextValue::PlainTextValue()
748 {
749     NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged);
750     readConfiguration();
751 }
752 
notificationEvent(int eventId)753 void PlainTextValue::notificationEvent(int eventId)
754 {
755     if (eventId == NotificationHub::EventConfigurationChanged)
756         readConfiguration();
757 }
758 
readConfiguration()759 void PlainTextValue::readConfiguration()
760 {
761     KSharedConfigPtr config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc")));
762     KConfigGroup configGroup(config, "General");
763     personNameFormatting = configGroup.readEntry(Preferences::keyPersonNameFormatting, Preferences::defaultPersonNameFormatting);
764 }
765 
766 QString PlainTextValue::personNameFormatting;
767 #else // HAVE_KF5
768 const QString PlainTextValue::personNameFormatting = QStringLiteral("<%l><, %s><, %f>");
769 #endif // HAVE_KF5
770