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 }