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