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

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 "idsuggestions.h"
0021 
0022 #include <QRegularExpression>
0023 
0024 #ifdef HAVE_KFI18N
0025 #include <KLocalizedString>
0026 #else // HAVE_KFI18N
0027 #define i18nc(comment,text) QStringLiteral(text)
0028 #define i18n(text) QStringLiteral(text)
0029 #endif // HAVE_KFI18N
0030 
0031 #include <Preferences>
0032 #include <Encoder>
0033 #include "journalabbreviations.h"
0034 
0035 class IdSuggestions::IdSuggestionsPrivate
0036 {
0037 private:
0038     IdSuggestions *p;
0039     static const QStringList smallWords;
0040 
0041 public:
0042 
0043     IdSuggestionsPrivate(IdSuggestions *parent)
0044             : p(parent) {
0045         /// nothing
0046     }
0047 
0048     static QString normalizeText(const QString &input) {
0049         static const QRegularExpression unwantedChars(QStringLiteral("[^-_:/=+a-zA-Z0-9]+"));
0050         return Encoder::instance().convertToPlainAscii(input).remove(unwantedChars);
0051     }
0052 
0053     static int numberFromEntry(const Entry &entry, const QString &field) {
0054         static const QRegularExpression firstDigits(QStringLiteral("^[0-9]+"));
0055         const QString text = PlainTextValue::text(entry.value(field));
0056         const QRegularExpressionMatch match = firstDigits.match(text);
0057         if (!match.hasMatch()) return -1;
0058 
0059         bool ok = false;
0060         const int result = match.captured(0).toInt(&ok);
0061         return ok ? result : -1;
0062     }
0063 
0064     static QString pageNumberFromEntry(const Entry &entry) {
0065         static const QRegularExpression whitespace(QStringLiteral("[ \t]+"));
0066         static const QRegularExpression pageNumber(QStringLiteral("[a-z0-9+:]+"), QRegularExpression::CaseInsensitiveOption);
0067         const QString text = PlainTextValue::text(entry.value(Entry::ftPages)).remove(whitespace).remove(QStringLiteral("mbox"));
0068         const QRegularExpressionMatch match = pageNumber.match(text);
0069         if (!match.hasMatch()) return QString();
0070         return match.captured(0);
0071     }
0072 
0073     static QString translateTitleToken(const Entry &entry, const struct IdSuggestionTokenInfo &tti, bool removeSmallWords) {
0074         QString result;
0075         bool first = true;
0076         static const QRegularExpression wordSeparator(QStringLiteral("\\W+"), QRegularExpression::UseUnicodePropertiesOption);
0077 #if QT_VERSION >= 0x050e00
0078         const QStringList titleWords = PlainTextValue::text(entry.value(Entry::ftTitle)).split(wordSeparator, Qt::SkipEmptyParts);
0079 #else // QT_VERSION < 0x050e00
0080         const QStringList titleWords = PlainTextValue::text(entry.value(Entry::ftTitle)).split(wordSeparator, QString::SkipEmptyParts);
0081 #endif // QT_VERSION >= 0x050e00
0082         int index = 0;
0083         for (QStringList::ConstIterator it = titleWords.begin(); it != titleWords.end(); ++it) {
0084             const QString lowerText = normalizeText(*it).toLower();
0085             if ((removeSmallWords && smallWords.contains(lowerText)) || index < tti.startWord || index > tti.endWord)
0086                 continue;
0087             ++index; ///< only increase index if actually considering current title word (not a 'small word')
0088 
0089             if (first)
0090                 first = false;
0091             else
0092                 result.append(tti.inBetween);
0093 
0094             QString titleComponent = lowerText.left(tti.len);
0095             if (tti.caseChange == IdSuggestions::CaseChange::ToCamelCase)
0096                 titleComponent = titleComponent[0].toUpper() + titleComponent.mid(1);
0097 
0098             result.append(titleComponent);
0099         }
0100 
0101         switch (tti.caseChange) {
0102         case IdSuggestions::CaseChange::ToUpper:
0103             result = result.toUpper();
0104             break;
0105         case IdSuggestions::CaseChange::ToLower:
0106             result = result.toLower();
0107             break;
0108         case IdSuggestions::CaseChange::ToCamelCase:
0109         /// already processed above
0110         case IdSuggestions::CaseChange::None:
0111             /// nothing
0112             break;
0113         }
0114 
0115         return result;
0116     }
0117 
0118     static QString translateAuthorsToken(const Entry &entry, const struct IdSuggestionTokenInfo &ati) {
0119         QString result;
0120         /// Already some author inserted into result?
0121         bool firstInserted = false;
0122         /// Get list of authors' last names
0123         const QStringList authors = entry.authorsLastName();
0124         /// Keep track of which author (number/position) is processed
0125         int index = 0;
0126         /// Go through all authors
0127         for (QStringList::ConstIterator it = authors.constBegin(); it != authors.constEnd(); ++it, ++index) {
0128             /// Get current author, normalize name (remove unwanted characters), cut to maximum length
0129             QString author = normalizeText(*it).left(ati.len);
0130             /// Check if camel case is requests
0131             if (ati.caseChange == IdSuggestions::CaseChange::ToCamelCase) {
0132                 /// Get components of the author's last name
0133 #if QT_VERSION >= 0x050e00
0134                 const QStringList nameComponents = author.split(QStringLiteral(" "), Qt::SkipEmptyParts);
0135 #else // QT_VERSION < 0x050e00
0136                 const QStringList nameComponents = author.split(QStringLiteral(" "), QString::SkipEmptyParts);
0137 #endif // QT_VERSION >= 0x050e00
0138                 QStringList newNameComponents;
0139                 newNameComponents.reserve(nameComponents.size());
0140                 /// Camel-case each name component
0141                 for (const QString &nameComponent : nameComponents) {
0142                     newNameComponents.append(nameComponent[0].toUpper() + nameComponent.mid(1));
0143                 }
0144                 /// Re-assemble name from camel-cased components
0145                 author = newNameComponents.join(QStringLiteral(" "));
0146             }
0147             if (
0148                 (index >= ati.startWord && index <= ati.endWord) ///< check for requested author range
0149                 || (ati.lastWord && index == authors.count() - 1) ///< explicitly insert last author if requested in lastWord flag
0150             ) {
0151                 if (firstInserted)
0152                     result.append(ati.inBetween);
0153                 result.append(author);
0154                 firstInserted = true;
0155             }
0156         }
0157 
0158         switch (ati.caseChange) {
0159         case IdSuggestions::CaseChange::ToUpper:
0160             result = result.toUpper();
0161             break;
0162         case IdSuggestions::CaseChange::ToLower:
0163             result = result.toLower();
0164             break;
0165         case IdSuggestions::CaseChange::ToCamelCase:
0166             /// already processed above
0167             break;
0168         case IdSuggestions::CaseChange::None:
0169             /// nothing
0170             break;
0171         }
0172 
0173         return result;
0174     }
0175 
0176     static QString translateJournalToken(const Entry &entry, const struct IdSuggestionTokenInfo &jti, bool removeSmallWords) {
0177         static const QRegularExpression wordSeparator(QStringLiteral("\\W+"), QRegularExpression::UseUnicodePropertiesOption);
0178         QString journalName = PlainTextValue::text(entry.value(Entry::ftJournal));
0179         journalName = JournalAbbreviations::instance().toShortName(journalName);
0180 #if QT_VERSION >= 0x050e00
0181         const QStringList journalWords = journalName.split(wordSeparator, Qt::SkipEmptyParts);
0182 #else // QT_VERSION < 0x050e00
0183         const QStringList journalWords = journalName.split(wordSeparator, QString::SkipEmptyParts);
0184 #endif // QT_VERSION >= 0x050e00
0185         bool first = true;
0186         int index = 0;
0187         QString result;
0188         for (QStringList::ConstIterator it = journalWords.begin(); it != journalWords.end(); ++it, ++index) {
0189             QString journalComponent = normalizeText(*it);
0190             const QString lowerText = journalComponent.toLower();
0191             if ((removeSmallWords && smallWords.contains(lowerText)) || index < jti.startWord || index > jti.endWord)
0192                 continue;
0193 
0194             if (first)
0195                 first = false;
0196             else
0197                 result.append(jti.inBetween);
0198 
0199             /// Try to keep sequences of capital letters at the start of the journal name,
0200             /// those may already be abbreviations.
0201             int countCaptialCharsAtStart = 0;
0202             while (journalComponent[countCaptialCharsAtStart].isUpper()) ++countCaptialCharsAtStart;
0203             journalComponent = journalComponent.left(qMax(jti.len, countCaptialCharsAtStart));
0204 
0205             if (jti.caseChange == IdSuggestions::CaseChange::ToCamelCase)
0206                 journalComponent = journalComponent[0].toUpper() + journalComponent.mid(1);
0207 
0208             result.append(journalComponent);
0209         }
0210 
0211         switch (jti.caseChange) {
0212         case IdSuggestions::CaseChange::ToUpper:
0213             result = result.toUpper();
0214             break;
0215         case IdSuggestions::CaseChange::ToLower:
0216             result = result.toLower();
0217             break;
0218         case IdSuggestions::CaseChange::ToCamelCase:
0219         /// already processed above
0220         case IdSuggestions::CaseChange::None:
0221             /// nothing
0222             break;
0223         }
0224 
0225         return result;
0226     }
0227 
0228     static QString translateTypeToken(const Entry &entry, const struct IdSuggestionTokenInfo &eti) {
0229         QString entryType(entry.type());
0230 
0231         switch (eti.caseChange) {
0232         case IdSuggestions::CaseChange::ToUpper:
0233             return entryType.toUpper().left(eti.len);
0234         case IdSuggestions::CaseChange::ToLower:
0235             return entryType.toLower().left(eti.len);
0236         case IdSuggestions::CaseChange::ToCamelCase:
0237         {
0238             if (entryType.isEmpty()) return QString(); ///< empty entry type? Return immediately to avoid problems with entryType[0]
0239             /// Apply some heuristic replacements to make the entry type look like CamelCase
0240             entryType = entryType.toLower(); ///< start with lower case
0241             /// Then, replace known words with their CamelCase variant
0242             entryType = entryType.replace(QStringLiteral("report"), QStringLiteral("Report")).replace(QStringLiteral("proceedings"), QStringLiteral("Proceedings")).replace(QStringLiteral("thesis"), QStringLiteral("Thesis")).replace(QStringLiteral("book"), QStringLiteral("Book")).replace(QStringLiteral("phd"), QStringLiteral("PhD"));
0243             /// Finally, guarantee that first letter is upper case
0244             entryType[0] = entryType[0].toUpper();
0245             return entryType.left(eti.len);
0246         }
0247         default:
0248             return entryType.left(eti.len);
0249         }
0250     }
0251 
0252     static QString translateToken(const Entry &entry, const QString &token) {
0253         switch (token[0].unicode()) {
0254         case 'a': ///< deprecated but still supported case
0255         {
0256             /// Evaluate the token string, store information in struct IdSuggestionTokenInfo ati
0257             struct IdSuggestionTokenInfo ati = IdSuggestions::evalToken(token.mid(1));
0258             ati.startWord = ati.endWord = 0; ///< only first author
0259             return translateAuthorsToken(entry, ati);
0260         }
0261         case 'A': {
0262             /// Evaluate the token string, store information in struct IdSuggestionTokenInfo ati
0263             const struct IdSuggestionTokenInfo ati = IdSuggestions::evalToken(token.mid(1));
0264             return translateAuthorsToken(entry, ati);
0265         }
0266         case 'z': ///< deprecated but still supported case
0267         {
0268             /// Evaluate the token string, store information in struct IdSuggestionTokenInfo ati
0269             struct IdSuggestionTokenInfo ati = IdSuggestions::evalToken(token.mid(1));
0270             /// All but first author
0271             ati.startWord = 1;
0272             ati.endWord = std::numeric_limits<int>::max();
0273             return translateAuthorsToken(entry, ati);
0274         }
0275         case 'y': {
0276             int year = numberFromEntry(entry, Entry::ftYear);
0277             if (year > -1)
0278                 return QString::number(year % 100 + 100).mid(1);
0279             break;
0280         }
0281         case 'Y': {
0282             const int year = numberFromEntry(entry, Entry::ftYear);
0283             if (year > -1)
0284                 return QString::number(year % 10000 + 10000).mid(1);
0285             break;
0286         }
0287         case 't':
0288         case 'T': {
0289             /// Evaluate the token string, store information in struct IdSuggestionTokenInfo jti
0290             const struct IdSuggestionTokenInfo tti = IdSuggestions::evalToken(token.mid(1));
0291             return translateTitleToken(entry, tti, token[0].isUpper());
0292         }
0293         case 'j':
0294         case 'J': {
0295             /// Evaluate the token string, store information in struct IdSuggestionTokenInfo jti
0296             const struct IdSuggestionTokenInfo jti = IdSuggestions::evalToken(token.mid(1));
0297             return translateJournalToken(entry, jti, token[0].isUpper());
0298         }
0299         case 'e': {
0300             /// Evaluate the token string, store information in struct IdSuggestionTokenInfo eti
0301             const struct IdSuggestionTokenInfo eti = IdSuggestions::evalToken(token.mid(1));
0302             return translateTypeToken(entry, eti);
0303         }
0304         case 'v': {
0305             return normalizeText(PlainTextValue::text(entry.value(Entry::ftVolume)));
0306         }
0307         case 'p': {
0308             return pageNumberFromEntry(entry);
0309         }
0310         case '"': return token.mid(1);
0311         }
0312 
0313         return QString();
0314     }
0315 };
0316 
0317 /// List of small words taken from OCLC:
0318 /// https://www.oclc.org/developer/develop/web-services/worldcat-search-api/bibliographic-resource.en.html
0319 const QStringList IdSuggestions::IdSuggestionsPrivate::smallWords = i18nc("Small words that can be removed from titles when generating id suggestions; separated by pipe symbol", "a|als|am|an|are|as|at|auf|aus|be|but|by|das|dass|de|der|des|dich|dir|du|er|es|for|from|had|have|he|her|his|how|ihr|ihre|ihres|im|in|is|ist|it|kein|la|le|les|mein|mich|mir|mit|of|on|sein|sie|that|the|this|to|un|une|von|was|wer|which|wie|wird|with|yousie|that|the|this|to|un|une|von|was|wer|which|wie|wird|with|you|und|and|ein|eine|einer|eines").split(QStringLiteral("|"),
0320 #if QT_VERSION >= 0x050e00
0321   Qt::SkipEmptyParts
0322 #else // QT_VERSION < 0x050e00
0323   QString::SkipEmptyParts
0324 #endif // QT_VERSION >= 0x050e00
0325 );
0326 
0327 QString IdSuggestions::formatId(const Entry &entry, const QString &formatStr)
0328 {
0329     QString id;
0330 #if QT_VERSION >= 0x050e00
0331     const QStringList tokenList = formatStr.split(QStringLiteral("|"), Qt::SkipEmptyParts);
0332 #else // QT_VERSION < 0x050e00
0333     const QStringList tokenList = formatStr.split(QStringLiteral("|"), QString::SkipEmptyParts);
0334 #endif // QT_VERSION >= 0x050e00
0335     for (const QString &token : tokenList) {
0336         id.append(IdSuggestionsPrivate::translateToken(entry, token));
0337     }
0338 
0339     return id;
0340 }
0341 
0342 QString IdSuggestions::defaultFormatId(const Entry &entry)
0343 {
0344     return formatId(entry, Preferences::instance().activeIdSuggestionFormatString());
0345 }
0346 
0347 bool IdSuggestions::hasDefaultFormat()
0348 {
0349     return !Preferences::instance().activeIdSuggestionFormatString().isEmpty();
0350 }
0351 
0352 bool IdSuggestions::applyDefaultFormatId(Entry &entry)
0353 {
0354     const QString dfs = Preferences::instance().activeIdSuggestionFormatString();
0355     if (!dfs.isEmpty()) {
0356         entry.setId(defaultFormatId(entry));
0357         return true;
0358     } else
0359         return false;
0360 }
0361 
0362 QStringList IdSuggestions::formatIdList(const Entry &entry)
0363 {
0364     const QStringList formatStrings = Preferences::instance().idSuggestionFormatStrings();
0365     QStringList result;
0366     result.reserve(formatStrings.size());
0367     for (const QString &formatString : formatStrings) {
0368         result << formatId(entry, formatString);
0369     }
0370     return result;
0371 }
0372 
0373 QStringList IdSuggestions::formatStrToHuman(const QString &formatStr)
0374 {
0375     QStringList result;
0376 #if QT_VERSION >= 0x050e00
0377     const QStringList tokenList = formatStr.split(QStringLiteral("|"), Qt::SkipEmptyParts);
0378 #else // QT_VERSION < 0x050e00
0379     const QStringList tokenList = formatStr.split(QStringLiteral("|"), QString::SkipEmptyParts);
0380 #endif // QT_VERSION >= 0x050e00
0381     for (const QString &token : tokenList) {
0382         QString text;
0383         if (token[0] == QLatin1Char('a') || token[0] == QLatin1Char('A') || token[0] == QLatin1Char('z')) {
0384             struct IdSuggestionTokenInfo info = evalToken(token.mid(1));
0385             if (token[0] == QLatin1Char('a'))
0386                 info.startWord = info.endWord = 0;
0387             else if (token[0] == QLatin1Char('z')) {
0388                 info.startWord = 1;
0389                 info.endWord = std::numeric_limits<int>::max();
0390             }
0391             text = formatAuthorRange(info.startWord, info.endWord, info.lastWord);
0392 
0393             if (info.len > 0 && info.len < std::numeric_limits<int>::max())
0394 #ifdef HAVE_KFI18N
0395                 text.append(i18np(", but only first letter of each last name", ", but only first %1 letters of each last name", info.len));
0396 #else // HAVE_KFI18N
0397                 text.append(info.len > 1 ? QString(QStringLiteral(", but only first %1 letters of each last name")).arg(QString::number(info.len)) : QStringLiteral(", but only first letter of each last name"));
0398 #endif // HAVE_KFI18N
0399 
0400             switch (info.caseChange) {
0401             case IdSuggestions::CaseChange::ToUpper:
0402                 text.append(i18n(", in upper case"));
0403                 break;
0404             case IdSuggestions::CaseChange::ToLower:
0405                 text.append(i18n(", in lower case"));
0406                 break;
0407             case IdSuggestions::CaseChange::ToCamelCase:
0408                 text.append(i18n(", in CamelCase"));
0409                 break;
0410             case IdSuggestions::CaseChange::None:
0411                 break;
0412             }
0413 
0414             if (!info.inBetween.isEmpty())
0415 #ifdef HAVE_KFI18N
0416                 text.append(i18n(", with '%1' in between", info.inBetween));
0417 #else // HAVE_KFI18N
0418                 text.append(QString(QStringLiteral(", with '%1' in between")).arg(info.inBetween));
0419 #endif // HAVE_KFI18N
0420         }
0421         else if (token[0] == QLatin1Char('y'))
0422             text.append(i18n("Year (2 digits)"));
0423         else if (token[0] == QLatin1Char('Y'))
0424             text.append(i18n("Year (4 digits)"));
0425         else if (token[0] == QLatin1Char('t') || token[0] == QLatin1Char('T')) {
0426             struct IdSuggestionTokenInfo info = evalToken(token.mid(1));
0427             text.append(i18n("Title"));
0428             if (info.startWord == 0 && info.endWord < std::numeric_limits<int>::max())
0429 #ifdef HAVE_KFI18N
0430                 text.append(i18np(", but only the first word", ", but only first %1 words", info.endWord + 1));
0431 #else // HAVE_KFI18N
0432                 text.append(info.endWord + 1 > 1 ? QString(QStringLiteral(", but only first %1 words'")).arg(QString::number(info.endWord + 1)) : QStringLiteral(", but only the first word"));
0433 #endif // HAVE_KFI18N
0434             else if (info.startWord > 0 && info.endWord == std::numeric_limits<int>::max())
0435 #ifdef HAVE_KFI18N
0436                 text.append(i18n(", but only starting from word %1", info.startWord + 1));
0437 #else // HAVE_KFI18N
0438                 text.append(QString(QStringLiteral(", but only starting from word %1")).arg(QString::number(info.startWord + 1)));
0439 #endif // HAVE_KFI18N
0440             else if (info.startWord > 0 && info.endWord < std::numeric_limits<int>::max())
0441 #ifdef HAVE_KFI18N
0442                 text.append(i18n(", but only from word %1 to word %2", info.startWord + 1, info.endWord + 1));
0443 #else // HAVE_KFI18N
0444                 text.append(QString(QStringLiteral(", but only from word %1 to word %2")).arg(QString::number(info.startWord + 1), QString::number(info.endWord + 1)));
0445 #endif // HAVE_KFI18N
0446             if (info.len > 0 && info.len < std::numeric_limits<int>::max())
0447 #ifdef HAVE_KFI18N
0448                 text.append(i18np(", but only first letter of each word", ", but only first %1 letters of each word", info.len));
0449 #else // HAVE_KFI18N
0450                 text.append(info.len > 1 ? QString(QStringLiteral(", but only first %1 letters of each word'")).arg(QString::number(info.len)) : QStringLiteral(", but only first letter of each word"));
0451 #endif // HAVE_KFI18N
0452 
0453             switch (info.caseChange) {
0454             case IdSuggestions::CaseChange::ToUpper:
0455                 text.append(i18n(", in upper case"));
0456                 break;
0457             case IdSuggestions::CaseChange::ToLower:
0458                 text.append(i18n(", in lower case"));
0459                 break;
0460             case IdSuggestions::CaseChange::ToCamelCase:
0461                 text.append(i18n(", in CamelCase"));
0462                 break;
0463             case IdSuggestions::CaseChange::None:
0464                 break;
0465             }
0466 
0467             if (!info.inBetween.isEmpty())
0468 #ifdef HAVE_KFI18N
0469                 text.append(i18n(", with '%1' in between", info.inBetween));
0470 #else // HAVE_KFI18N
0471                 text.append(QString(QStringLiteral(", with '%1' in between")).arg(info.inBetween));
0472 #endif // HAVE_KFI18N
0473             if (token[0] == QLatin1Char('T')) text.append(i18n(", small words removed"));
0474         }
0475         else if (token[0] == QLatin1Char('j')) {
0476             struct IdSuggestionTokenInfo info = evalToken(token.mid(1));
0477             text.append(i18n("Journal"));
0478             if (info.len > 0 && info.len < std::numeric_limits<int>::max())
0479 #ifdef HAVE_KFI18N
0480                 text.append(i18np(", but only first letter of each word", ", but only first %1 letters of each word", info.len));
0481 #else // HAVE_KFI18N
0482                 text.append(info.len > 1 ? QString(QStringLiteral(", but only first %1 letters of each word'")).arg(QString::number(info.len)) : QStringLiteral(", but only first letter of each word"));
0483 #endif // HAVE_KFI18N
0484             switch (info.caseChange) {
0485             case IdSuggestions::CaseChange::ToUpper:
0486                 text.append(i18n(", in upper case"));
0487                 break;
0488             case IdSuggestions::CaseChange::ToLower:
0489                 text.append(i18n(", in lower case"));
0490                 break;
0491             case IdSuggestions::CaseChange::ToCamelCase:
0492                 text.append(i18n(", in CamelCase"));
0493                 break;
0494             case IdSuggestions::CaseChange::None:
0495                 break;
0496             }
0497         } else if (token[0] == QLatin1Char('e')) {
0498             struct IdSuggestionTokenInfo info = evalToken(token.mid(1));
0499             text.append(i18n("Type"));
0500             if (info.len > 0 && info.len < std::numeric_limits<int>::max())
0501 #ifdef HAVE_KFI18N
0502                 text.append(i18np(", but only first letter of each word", ", but only first %1 letters of each word", info.len));
0503 #else // HAVE_KFI18N
0504                 text.append(info.len > 1 ? QString(QStringLiteral(", but only first %1 letters of each word'")).arg(QString::number(info.len)) : QStringLiteral(", but only first letter of each word"));
0505 #endif // HAVE_KFI18N
0506             switch (info.caseChange) {
0507             case IdSuggestions::CaseChange::ToUpper:
0508                 text.append(i18n(", in upper case"));
0509                 break;
0510             case IdSuggestions::CaseChange::ToLower:
0511                 text.append(i18n(", in lower case"));
0512                 break;
0513             case IdSuggestions::CaseChange::ToCamelCase:
0514                 text.append(i18n(", in CamelCase"));
0515                 break;
0516             default:
0517                 break;
0518             }
0519         } else if (token[0] == QLatin1Char('v')) {
0520             text.append(i18n("Volume"));
0521         } else if (token[0] == QLatin1Char('p')) {
0522             text.append(i18n("First page number"));
0523         } else if (token[0] == QLatin1Char('"'))
0524 #ifdef HAVE_KFI18N
0525             text.append(i18n("Text: '%1'", token.mid(1)));
0526 #else // HAVE_KFI18N
0527             text.append(QString(QStringLiteral("Text: '%1'")).arg(token.mid(1)));
0528 #endif // HAVE_KFI18N
0529         else
0530             text.append(QStringLiteral("?"));
0531 
0532         result.append(text);
0533     }
0534 
0535     return result;
0536 }
0537 
0538 QString IdSuggestions::formatAuthorRange(int minValue, int maxValue, bool lastAuthor) {
0539     if (minValue == 0) {
0540         if (maxValue == 0) {
0541             if (lastAuthor)
0542                 return i18n("First and last authors only");
0543             else
0544                 return i18n("First author only");
0545         } else if (maxValue == std::numeric_limits<int>::max())
0546             return i18n("All authors");
0547         else {
0548             if (lastAuthor)
0549 #ifdef HAVE_KFI18N
0550                 return i18n("From first author to author %1 and last author", maxValue + 1);
0551 #else // HAVE_KFI18N
0552                 return QString(QStringLiteral("From first author to author %1 and last author")).arg(QString::number(maxValue + 1));
0553 #endif // HAVE_KFI18N
0554             else
0555 #ifdef HAVE_KFI18N
0556                 return i18n("From first author to author %1", maxValue + 1);
0557 #else // HAVE_KFI18N
0558                 return QString(QStringLiteral("From first author to author %1")).arg(QString::number(maxValue + 1));
0559 #endif // HAVE_KFI18N
0560         }
0561     } else if (minValue == 1) {
0562         if (maxValue == std::numeric_limits<int>::max())
0563             return i18n("All but first author");
0564         else {
0565             if (lastAuthor)
0566 #ifdef HAVE_KFI18N
0567                 return i18n("From author %1 to author %2 and last author", minValue + 1, maxValue + 1);
0568 #else // HAVE_KFI18N
0569                 return QString(QStringLiteral("From author %1 to author %2 and last author")).arg(QString::number(minValue + 1), QString::number(maxValue + 1));
0570 #endif // HAVE_KFI18N
0571             else
0572 #ifdef HAVE_KFI18N
0573                 return i18n("From author %1 to author %2", minValue + 1, maxValue + 1);
0574 #else // HAVE_KFI18N
0575                 return QString(QStringLiteral("From author %1 to author %2")).arg(QString::number(minValue + 1), QString::number(maxValue + 1));
0576 #endif // HAVE_KFI18N
0577         }
0578     } else {
0579         if (maxValue == std::numeric_limits<int>::max())
0580 #ifdef HAVE_KFI18N
0581             return i18n("From author %1 to last author", minValue + 1);
0582 #else // HAVE_KFI18N
0583             return QString(QStringLiteral("From author %1 to last author")).arg(QString::number(minValue + 1));
0584 #endif // HAVE_KFI18N
0585         else if (lastAuthor)
0586 #ifdef HAVE_KFI18N
0587             return i18n("From author %1 to author %2 and last author", minValue + 1, maxValue + 1);
0588 #else // HAVE_KFI18N
0589             return QString(QStringLiteral("From author %1 to author %2 and last author")).arg(QString::number(minValue + 1), QString::number(maxValue + 1));
0590 #endif // HAVE_KFI18N
0591         else
0592 #ifdef HAVE_KFI18N
0593             return i18n("From author %1 to author %2", minValue + 1, maxValue + 1);
0594 #else // HAVE_KFI18N
0595             return QString(QStringLiteral("From author %1 to author %2")).arg(QString::number(minValue + 1), QString::number(maxValue + 1));
0596 #endif // HAVE_KFI18N
0597     }
0598 }
0599 
0600 IdSuggestions::IdSuggestionTokenInfo IdSuggestions::evalToken(const QString &token) {
0601     int pos = 0;
0602     struct IdSuggestionTokenInfo result;
0603     result.len = std::numeric_limits<int>::max();
0604     result.startWord = 0;
0605     result.endWord = std::numeric_limits<int>::max();
0606     result.lastWord = false;
0607     result.caseChange = IdSuggestions::CaseChange::None;
0608     result.inBetween = QString();
0609 
0610     if (token.length() > pos) {
0611         int dv = token[pos].digitValue();
0612         if (dv > -1) {
0613             result.len = dv;
0614             ++pos;
0615         }
0616     }
0617 
0618     if (token.length() > pos) {
0619         switch (token[pos].unicode()) {
0620         case 0x006c: // 'l'
0621             result.caseChange = IdSuggestions::CaseChange::ToLower;
0622             ++pos;
0623             break;
0624         case 0x0075: // 'u'
0625             result.caseChange = IdSuggestions::CaseChange::ToUpper;
0626             ++pos;
0627             break;
0628         case 0x0063: // 'c'
0629             result.caseChange = IdSuggestions::CaseChange::ToCamelCase;
0630             ++pos;
0631             break;
0632         default:
0633             result.caseChange = IdSuggestions::CaseChange::None;
0634         }
0635     }
0636 
0637     int dvStart = 0, dvEnd = std::numeric_limits<int>::max();
0638     if (token.length() > pos + 2 ///< sufficiently many characters to follow
0639             && token[pos] == QLatin1Char('w') ///< identifier to start specifying a range of words
0640             && (dvStart = token[pos + 1].digitValue()) > -1 ///< first word index correctly parsed
0641             && (
0642                 token[pos + 2] == QLatin1Char('I') ///< infinitely many words
0643                 || (dvEnd = token[pos + 2].digitValue()) > -1) ///< last word index finite and correctly parsed
0644        ) {
0645         result.startWord = dvStart;
0646         result.endWord = dvEnd;
0647         pos += 3;
0648 
0649         /// Optionally, the last word (e.g. last author) is explicitly requested
0650         if (token.length() > pos && token[pos] == QLatin1Char('L')) {
0651             result.lastWord = true;
0652             ++pos;
0653         }
0654     }
0655 
0656     if (token.length() > pos + 1 && token[pos] == QLatin1Char('"'))
0657         result.inBetween = token.mid(pos + 1);
0658 
0659     return result;
0660 }