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 "fileexporterbibtex.h"
19 
20 #include <typeinfo>
21 
22 #include <QTextCodec>
23 #include <QTextStream>
24 #include <QStringList>
25 
26 #ifdef HAVE_KF5
27 #include <KSharedConfig>
28 #include <KConfigGroup>
29 #endif // HAVE_KF5
30 
31 #include "preferences.h"
32 #include "file.h"
33 #include "element.h"
34 #include "entry.h"
35 #include "macro.h"
36 #include "preamble.h"
37 #include "value.h"
38 #include "comment.h"
39 #include "encoderlatex.h"
40 #include "bibtexentries.h"
41 #include "bibtexfields.h"
42 #include "textencoder.h"
43 #include "logging_io.h"
44 
45 FileExporterBibTeX *FileExporterBibTeX::staticFileExporterBibTeX = nullptr;
46 
47 class FileExporterBibTeX::FileExporterBibTeXPrivate
48 {
49 private:
50     FileExporterBibTeX *p;
51 
52 public:
53     QChar stringOpenDelimiter;
54     QChar stringCloseDelimiter;
55     KBibTeX::Casing keywordCasing;
56     Preferences::QuoteComment quoteComment;
57     QString encoding, forcedEncoding;
58     Qt::CheckState protectCasing;
59     QString personNameFormatting;
60     QString listSeparator;
61     bool cancelFlag;
62     QTextCodec *destinationCodec;
63 #ifdef HAVE_KF5
64     KSharedConfigPtr config;
65     const QString configGroupName, configGroupNameGeneral;
66 #endif // HAVE_KF5
67 
FileExporterBibTeXPrivate(FileExporterBibTeX * parent)68     FileExporterBibTeXPrivate(FileExporterBibTeX *parent)
69             : p(parent), keywordCasing(KBibTeX::cLowerCase), quoteComment(Preferences::qcNone), protectCasing(Qt::PartiallyChecked), cancelFlag(false), destinationCodec(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("FileExporterBibTeX")), configGroupNameGeneral(QStringLiteral("General")) {
70         /// nothing
71     }
72 
loadState()73     void loadState() {
74 #ifdef HAVE_KF5
75         KConfigGroup configGroup(config, configGroupName);
76         encoding = configGroup.readEntry(Preferences::keyEncoding, Preferences::defaultEncoding);
77         QString stringDelimiter = configGroup.readEntry(Preferences::keyStringDelimiter, Preferences::defaultStringDelimiter);
78         if (stringDelimiter.length() != 2)
79             stringDelimiter = Preferences::defaultStringDelimiter;
80 #else // HAVE_KF5
81         encoding = QStringLiteral("LaTeX");
82         const QString stringDelimiter = QStringLiteral("{}");
83 #endif // HAVE_KF5
84         stringOpenDelimiter = stringDelimiter[0];
85         stringCloseDelimiter = stringDelimiter[1];
86 #ifdef HAVE_KF5
87         keywordCasing = static_cast<KBibTeX::Casing>(configGroup.readEntry(Preferences::keyKeywordCasing, static_cast<int>(Preferences::defaultKeywordCasing)));
88         quoteComment = static_cast<Preferences::QuoteComment>(configGroup.readEntry(Preferences::keyQuoteComment, static_cast<int>(Preferences::defaultQuoteComment)));
89         protectCasing = static_cast<Qt::CheckState>(configGroup.readEntry(Preferences::keyProtectCasing, static_cast<int>(Preferences::defaultProtectCasing)));
90         personNameFormatting = configGroup.readEntry(Preferences::keyPersonNameFormatting, QString());
91         listSeparator = configGroup.readEntry(Preferences::keyListSeparator, Preferences::defaultListSeparator);
92 
93         if (personNameFormatting.isEmpty()) {
94             /// no person name formatting is specified for BibTeX, fall back to general setting
95             KConfigGroup configGroupGeneral(config, configGroupNameGeneral);
96             personNameFormatting = configGroupGeneral.readEntry(Preferences::keyPersonNameFormatting, Preferences::defaultPersonNameFormatting);
97         }
98 #else // HAVE_KF5
99         keywordCasing = KBibTeX::cLowerCase;
100         quoteComment = qcNone;
101         protectCasing = Qt::PartiallyChecked;
102         personNameFormatting = QStringLiteral("<%l><, %s><, %f>");
103         listSeparator = QStringLiteral("; ");
104 #endif // HAVE_KF5
105     }
106 
loadStateFromFile(const File * bibtexfile)107     void loadStateFromFile(const File *bibtexfile) {
108         if (bibtexfile == nullptr) return;
109 
110         if (bibtexfile->hasProperty(File::Encoding))
111             encoding = bibtexfile->property(File::Encoding).toString();
112         if (!forcedEncoding.isEmpty())
113             encoding = forcedEncoding;
114         applyEncoding(encoding);
115         if (bibtexfile->hasProperty(File::StringDelimiter)) {
116             QString stringDelimiter = bibtexfile->property(File::StringDelimiter).toString();
117             if (stringDelimiter.length() != 2)
118 #ifdef HAVE_KF5
119                 stringDelimiter = Preferences::defaultStringDelimiter;
120 #else // HAVE_KF5
121                 stringDelimiter = QStringLiteral("{}");
122 #endif // HAVE_KF5
123             stringOpenDelimiter = stringDelimiter[0];
124             stringCloseDelimiter = stringDelimiter[1];
125         }
126         if (bibtexfile->hasProperty(File::QuoteComment))
127             quoteComment = static_cast<Preferences::QuoteComment>(bibtexfile->property(File::QuoteComment).toInt());
128         if (bibtexfile->hasProperty(File::KeywordCasing))
129             keywordCasing = static_cast<KBibTeX::Casing>(bibtexfile->property(File::KeywordCasing).toInt());
130         if (bibtexfile->hasProperty(File::ProtectCasing))
131             protectCasing = static_cast<Qt::CheckState>(bibtexfile->property(File::ProtectCasing).toInt());
132         if (bibtexfile->hasProperty(File::NameFormatting)) {
133             /// if the user set "use global default", this property is an empty string
134             /// in this case, keep default value
135             const QString buffer = bibtexfile->property(File::NameFormatting).toString();
136             personNameFormatting = buffer.isEmpty() ? personNameFormatting : buffer;
137         }
138         if (bibtexfile->hasProperty(File::ListSeparator))
139             listSeparator = bibtexfile->property(File::ListSeparator).toString();
140     }
141 
writeEntry(QIODevice * iodevice,const Entry & entry)142     bool writeEntry(QIODevice *iodevice, const Entry &entry) {
143         const EncoderLaTeX &laTeXEncoder = EncoderLaTeX::instance();
144 
145         /// write start of a entry (entry type and id) in plain ASCII
146         iodevice->putChar('@');
147         iodevice->write(BibTeXEntries::instance().format(entry.type(), keywordCasing).toLatin1().data());
148         iodevice->putChar('{');
149         iodevice->write(laTeXEncoder.convertToPlainAscii(entry.id()).toLatin1());
150 
151         for (Entry::ConstIterator it = entry.constBegin(); it != entry.constEnd(); ++it) {
152             const QString key = it.key();
153             Value value = it.value();
154             if (value.isEmpty()) continue; ///< ignore empty key-value pairs
155 
156             QString text = p->internalValueToBibTeX(value, key, leUTF8);
157             if (text.isEmpty()) {
158                 /// ignore empty key-value pairs
159                 qCWarning(LOG_KBIBTEX_IO) << "Value for field " << key << " is empty" << endl;
160                 continue;
161             }
162 
163             // FIXME hack!
164             const QSharedPointer<ValueItem> first = *value.constBegin();
165             if (PlainText::isPlainText(*first) && (key == Entry::ftTitle || key == Entry::ftBookTitle || key == Entry::ftSeries)) {
166                 if (protectCasing == Qt::Checked)
167                     addProtectiveCasing(text);
168                 else if (protectCasing == Qt::Unchecked)
169                     removeProtectiveCasing(text);
170             }
171 
172             iodevice->putChar(',');
173             iodevice->putChar('\n');
174             iodevice->putChar('\t');
175             iodevice->write(laTeXEncoder.convertToPlainAscii(BibTeXFields::instance().format(key, keywordCasing)).toLatin1());
176             iodevice->putChar(' ');
177             iodevice->putChar('=');
178             iodevice->putChar(' ');
179             iodevice->write(TextEncoder::encode(text, destinationCodec));
180         }
181         iodevice->putChar('\n');
182         iodevice->putChar('}');
183         iodevice->putChar('\n');
184         iodevice->putChar('\n');
185 
186         return true;
187     }
188 
writeMacro(QIODevice * iodevice,const Macro & macro)189     bool writeMacro(QIODevice *iodevice, const Macro &macro) {
190         QString text = p->internalValueToBibTeX(macro.value(), QString(), leUTF8);
191         if (protectCasing == Qt::Checked)
192             addProtectiveCasing(text);
193         else if (protectCasing == Qt::Unchecked)
194             removeProtectiveCasing(text);
195 
196         iodevice->putChar('@');
197         iodevice->write(BibTeXEntries::instance().format(QStringLiteral("String"), keywordCasing).toLatin1().data());
198         iodevice->putChar('{');
199         iodevice->write(TextEncoder::encode(macro.key(), destinationCodec));
200         iodevice->putChar(' ');
201         iodevice->putChar('=');
202         iodevice->putChar(' ');
203         iodevice->write(TextEncoder::encode(text, destinationCodec));
204         iodevice->putChar('}');
205         iodevice->putChar('\n');
206         iodevice->putChar('\n');
207 
208         return true;
209     }
210 
writeComment(QIODevice * iodevice,const Comment & comment)211     bool writeComment(QIODevice *iodevice, const Comment &comment) {
212         QString text = comment.text() ;
213 
214         if (comment.useCommand() || quoteComment == Preferences::qcCommand) {
215             iodevice->putChar('@');
216             iodevice->write(BibTeXEntries::instance().format(QStringLiteral("Comment"), keywordCasing).toLatin1().data());
217             iodevice->putChar('{');
218             iodevice->write(TextEncoder::encode(text, destinationCodec));
219             iodevice->putChar('}');
220             iodevice->putChar('\n');
221             iodevice->putChar('\n');
222         } else if (quoteComment == Preferences::qcPercentSign) {
223             QStringList commentLines = text.split('\n', QString::SkipEmptyParts);
224             for (QStringList::Iterator it = commentLines.begin(); it != commentLines.end(); ++it) {
225                 const QByteArray line = TextEncoder::encode(*it, destinationCodec);
226                 if (line.length() == 0 || line[0] != QLatin1Char('%')) {
227                     /// Guarantee that every line starts with
228                     /// a percent sign
229                     iodevice->putChar('%');
230                 }
231                 iodevice->write(line);
232                 iodevice->putChar('\n');
233             }
234             iodevice->putChar('\n');
235         } else {
236             iodevice->write(TextEncoder::encode(text, destinationCodec));
237             iodevice->putChar('\n');
238             iodevice->putChar('\n');
239         }
240 
241         return true;
242     }
243 
writePreamble(QIODevice * iodevice,const Preamble & preamble)244     bool writePreamble(QIODevice *iodevice, const Preamble &preamble) {
245         iodevice->putChar('@');
246         iodevice->write(BibTeXEntries::instance().format(QStringLiteral("Preamble"), keywordCasing).toLatin1().data());
247         iodevice->putChar('{');
248         /// Remember: strings from preamble do not get encoded,
249         /// may contain raw LaTeX commands and code
250         iodevice->write(TextEncoder::encode(p->internalValueToBibTeX(preamble.value(), QString(), leRaw), destinationCodec));
251         iodevice->putChar('}');
252         iodevice->putChar('\n');
253         iodevice->putChar('\n');
254 
255         return true;
256     }
257 
addProtectiveCasing(QString & text)258     QString addProtectiveCasing(QString &text) {
259         /// Check if either
260         ///  - text is too short (less than two characters)  or
261         ///  - text neither starts/stops with double quotation marks
262         ///    nor starts with { and stops with }
263         if (text.length() < 2 || ((text[0] != QLatin1Char('"') || text[text.length() - 1] != QLatin1Char('"')) && (text[0] != QLatin1Char('{') || text[text.length() - 1] != QLatin1Char('}')))) {
264             /// Nothing to protect, as this is no text string
265             return text;
266         }
267 
268         bool addBrackets = true;
269 
270         if (text[1] == QLatin1Char('{') && text[text.length() - 2] == QLatin1Char('}')) {
271             /// If the given text looks like this:  {{...}}  or  "{...}"
272             /// still check that it is not like this: {{..}..{..}}
273             addBrackets = false;
274             for (int i = text.length() - 2, count = 0; !addBrackets && i > 1; --i) {
275                 if (text[i] == QLatin1Char('{')) ++count;
276                 else if (text[i] == QLatin1Char('}')) --count;
277                 if (count == 0) addBrackets = true;
278             }
279         }
280 
281         if (addBrackets)
282             text.insert(1, QStringLiteral("{")).insert(text.length() - 1,  QStringLiteral("}"));
283 
284         return text;
285     }
286 
removeProtectiveCasing(QString & text)287     QString removeProtectiveCasing(QString &text) {
288         /// Check if either
289         ///  - text is too short (less than two characters)  or
290         ///  - text neither starts/stops with double quotation marks
291         ///    nor starts with { and stops with }
292         if (text.length() < 2 || ((text[0] != QLatin1Char('"') || text[text.length() - 1] != QLatin1Char('"')) && (text[0] != QLatin1Char('{') || text[text.length() - 1] != QLatin1Char('}')))) {
293             /// Nothing to protect, as this is no text string
294             return text;
295         }
296 
297         if (text[1] != QLatin1Char('{') || text[text.length() - 2] != QLatin1Char('}'))
298             /// Nothing to remove
299             return text;
300 
301         /// If the given text looks like this:  {{...}}  or  "{...}"
302         /// still check that it is not like this: {{..}..{..}}
303         bool removeBrackets = true;
304         for (int i = text.length() - 2, count = 0; removeBrackets && i > 1; --i) {
305             if (text[i] == QLatin1Char('{')) ++count;
306             else if (text[i] == QLatin1Char('}')) --count;
307             if (count == 0) removeBrackets = false;
308         }
309 
310         if (removeBrackets)
311             text.remove(text.length() - 2, 1).remove(1, 1);
312 
313         return text;
314     }
315 
protectQuotationMarks(QString & text)316     QString &protectQuotationMarks(QString &text) {
317         int p = -1;
318         while ((p = text.indexOf(QLatin1Char('"'), p + 1)) > 0)
319             if (p == 0 || text[p - 1] != QLatin1Char('\\')) {
320                 text.insert(p + 1, QStringLiteral("}")).insert(p, QStringLiteral("{"));
321                 ++p;
322             }
323         return text;
324     }
325 
applyEncoding(QString & encoding)326     void applyEncoding(QString &encoding) {
327         encoding = encoding.isEmpty() ? QStringLiteral("latex") : encoding.toLower();
328         destinationCodec = QTextCodec::codecForName(encoding == QStringLiteral("latex") ? "us-ascii" : encoding.toLatin1());
329     }
330 
requiresPersonQuoting(const QString & text,bool isLastName)331     bool requiresPersonQuoting(const QString &text, bool isLastName) {
332         if (isLastName && !text.contains(QChar(' ')))
333             /** Last name contains NO spaces, no quoting necessary */
334             return false;
335         else if (!isLastName && !text.contains(QStringLiteral(" and ")))
336             /** First name contains no " and " no quoting necessary */
337             return false;
338         else if (isLastName && !text.isEmpty() && text[0].isLower())
339             /** Last name starts with lower-case character (von, van, de, ...) */
340             // FIXME does not work yet
341             return false;
342         else if (text[0] != '{' || text[text.length() - 1] != '}')
343             /** as either last name contains spaces or first name contains " and " and there is no protective quoting yet, there must be a protective quoting added */
344             return true;
345 
346         int bracketCounter = 0;
347         for (int i = text.length() - 1; i >= 0; --i) {
348             if (text[i] == '{')
349                 ++bracketCounter;
350             else if (text[i] == '}')
351                 --bracketCounter;
352             if (bracketCounter == 0 && i > 0)
353                 return true;
354         }
355         return false;
356     }
357 };
358 
359 
FileExporterBibTeX(QObject * parent)360 FileExporterBibTeX::FileExporterBibTeX(QObject *parent)
361         : FileExporter(parent), d(new FileExporterBibTeXPrivate(this))
362 {
363     /// nothing
364 }
365 
~FileExporterBibTeX()366 FileExporterBibTeX::~FileExporterBibTeX()
367 {
368     delete d;
369 }
370 
setEncoding(const QString & encoding)371 void FileExporterBibTeX::setEncoding(const QString &encoding)
372 {
373     d->forcedEncoding = encoding;
374 }
375 
save(QIODevice * iodevice,const File * bibtexfile,QStringList * errorLog)376 bool FileExporterBibTeX::save(QIODevice *iodevice, const File *bibtexfile, QStringList *errorLog)
377 {
378     Q_UNUSED(errorLog)
379 
380     if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) {
381         qCWarning(LOG_KBIBTEX_IO) << "Output device not writable";
382         return false;
383     }
384 
385     bool result = true;
386     const int totalElements = bibtexfile->count();
387     int currentPos = 0;
388 
389     d->loadState();
390     d->loadStateFromFile(bibtexfile);
391 
392     if (d->encoding != QStringLiteral("latex")) {
393         Comment encodingComment(QStringLiteral("x-kbibtex-encoding=") + d->encoding, true);
394         result &= d->writeComment(iodevice, encodingComment);
395     }
396 
397     /// Memorize which entries are used in a crossref field
398     QStringList crossRefIdList;
399     for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !d->cancelFlag; ++it) {
400         QSharedPointer<const Entry> entry = (*it).dynamicCast<const Entry>();
401         if (!entry.isNull()) {
402             const QString crossRef = PlainTextValue::text(entry->value(Entry::ftCrossRef));
403             if (!crossRef.isEmpty())
404                 crossRefIdList << crossRef;
405         }
406     }
407 
408     bool allPreamblesAndMacrosProcessed = false;
409     for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !d->cancelFlag; ++it) {
410         QSharedPointer<const Element> element = (*it);
411         QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>();
412 
413         if (!entry.isNull()) {
414             /// Postpone entries that are crossref'ed
415             if (crossRefIdList.contains(entry->id())) continue;
416 
417             if (!allPreamblesAndMacrosProcessed) {
418                 /// Guarantee that all macros and the preamble are written
419                 /// before the first entry (@article, ...) is written
420                 for (File::ConstIterator msit = it + 1; msit != bibtexfile->constEnd() && result && !d->cancelFlag; ++msit) {
421                     QSharedPointer<const Preamble> preamble = (*msit).dynamicCast<const Preamble>();
422                     if (!preamble.isNull()) {
423                         result &= d->writePreamble(iodevice, *preamble);
424                         emit progress(++currentPos, totalElements);
425                     } else {
426                         QSharedPointer<const Macro> macro = (*msit).dynamicCast<const Macro>();
427                         if (!macro.isNull()) {
428                             result &= d->writeMacro(iodevice, *macro);
429                             emit progress(++currentPos, totalElements);
430                         }
431                     }
432                 }
433                 allPreamblesAndMacrosProcessed = true;
434             }
435 
436             result &= d->writeEntry(iodevice, *entry);
437             emit progress(++currentPos, totalElements);
438         } else {
439             QSharedPointer<const Comment> comment = element.dynamicCast<const Comment>();
440             if (!comment.isNull() && !comment->text().startsWith(QStringLiteral("x-kbibtex-"))) {
441                 result &= d->writeComment(iodevice, *comment);
442                 emit progress(++currentPos, totalElements);
443             } else if (!allPreamblesAndMacrosProcessed) {
444                 QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>();
445                 if (!preamble.isNull()) {
446                     result &= d->writePreamble(iodevice, *preamble);
447                     emit progress(++currentPos, totalElements);
448                 } else {
449                     QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>();
450                     if (!macro.isNull()) {
451                         result &= d->writeMacro(iodevice, *macro);
452                         emit progress(++currentPos, totalElements);
453                     }
454                 }
455             }
456         }
457     }
458 
459     /// Crossref'ed entries are written last
460     for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !d->cancelFlag; ++it) {
461         QSharedPointer<const Entry> entry = (*it).dynamicCast<const Entry>();
462         if (entry.isNull()) continue;
463         if (!crossRefIdList.contains(entry->id())) continue;
464 
465         result &= d->writeEntry(iodevice, *entry);
466         emit progress(++currentPos, totalElements);
467     }
468 
469     iodevice->close();
470     return result && !d->cancelFlag;
471 }
472 
save(QIODevice * iodevice,const QSharedPointer<const Element> element,const File * bibtexfile,QStringList * errorLog)473 bool FileExporterBibTeX::save(QIODevice *iodevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog)
474 {
475     Q_UNUSED(errorLog)
476 
477     if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) {
478         qCWarning(LOG_KBIBTEX_IO) << "Output device not writable";
479         return false;
480     }
481 
482     bool result = false;
483 
484     d->loadState();
485     d->loadStateFromFile(bibtexfile);
486 
487     if (!d->forcedEncoding.isEmpty())
488         d->encoding = d->forcedEncoding;
489     d->applyEncoding(d->encoding);
490 
491     const QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>();
492     if (!entry.isNull())
493         result |= d->writeEntry(iodevice, *entry);
494     else {
495         const QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>();
496         if (!macro.isNull())
497             result |= d->writeMacro(iodevice, *macro);
498         else {
499             const QSharedPointer<const Comment> comment = element.dynamicCast<const Comment>();
500             if (!comment.isNull())
501                 result |= d->writeComment(iodevice, *comment);
502             else {
503                 const QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>();
504                 if (!preamble.isNull())
505                     result |= d->writePreamble(iodevice, *preamble);
506             }
507         }
508     }
509 
510     iodevice->close();
511     return result && !d->cancelFlag;
512 }
513 
cancel()514 void FileExporterBibTeX::cancel()
515 {
516     d->cancelFlag = true;
517 }
518 
valueToBibTeX(const Value & value,const QString & key,UseLaTeXEncoding useLaTeXEncoding)519 QString FileExporterBibTeX::valueToBibTeX(const Value &value, const QString &key, UseLaTeXEncoding useLaTeXEncoding)
520 {
521     if (staticFileExporterBibTeX == nullptr) {
522         staticFileExporterBibTeX = new FileExporterBibTeX(nullptr);
523         staticFileExporterBibTeX->d->loadState();
524     }
525     return staticFileExporterBibTeX->internalValueToBibTeX(value, key, useLaTeXEncoding);
526 }
527 
applyEncoder(const QString & input,UseLaTeXEncoding useLaTeXEncoding) const528 QString FileExporterBibTeX::applyEncoder(const QString &input, UseLaTeXEncoding useLaTeXEncoding) const {
529     switch (useLaTeXEncoding) {
530     case leLaTeX: return EncoderLaTeX::instance().encode(input, Encoder::TargetEncodingASCII);
531     case leUTF8: return EncoderLaTeX::instance().encode(input, Encoder::TargetEncodingUTF8);
532     default: return input;
533     }
534 }
535 
internalValueToBibTeX(const Value & value,const QString & key,UseLaTeXEncoding useLaTeXEncoding)536 QString FileExporterBibTeX::internalValueToBibTeX(const Value &value, const QString &key, UseLaTeXEncoding useLaTeXEncoding)
537 {
538     if (value.isEmpty())
539         return QString();
540 
541     QString result;
542     bool isOpen = false;
543     QSharedPointer<const ValueItem> prev;
544     for (const auto &valueItem : value) {
545         QSharedPointer<const MacroKey> macroKey = valueItem.dynamicCast<const MacroKey>();
546         if (!macroKey.isNull()) {
547             if (isOpen) result.append(d->stringCloseDelimiter);
548             isOpen = false;
549             if (!result.isEmpty()) result.append(" # ");
550             result.append(macroKey->text());
551             prev = macroKey;
552         } else {
553             QSharedPointer<const PlainText> plainText = valueItem.dynamicCast<const PlainText>();
554             if (!plainText.isNull()) {
555                 QString textBody = applyEncoder(plainText->text(), useLaTeXEncoding);
556                 if (!isOpen) {
557                     if (!result.isEmpty()) result.append(" # ");
558                     result.append(d->stringOpenDelimiter);
559                 } else if (!prev.dynamicCast<const PlainText>().isNull())
560                     result.append(' ');
561                 else if (!prev.dynamicCast<const Person>().isNull()) {
562                     /// handle "et al." i.e. "and others"
563                     result.append(" and ");
564                 } else {
565                     result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter);
566                 }
567                 isOpen = true;
568 
569                 if (d->stringOpenDelimiter == QLatin1Char('"'))
570                     d->protectQuotationMarks(textBody);
571                 result.append(textBody);
572                 prev = plainText;
573             } else {
574                 QSharedPointer<const VerbatimText> verbatimText = valueItem.dynamicCast<const VerbatimText>();
575                 if (!verbatimText.isNull()) {
576                     QString textBody = verbatimText->text();
577                     if (!isOpen) {
578                         if (!result.isEmpty()) result.append(" # ");
579                         result.append(d->stringOpenDelimiter);
580                     } else if (!prev.dynamicCast<const VerbatimText>().isNull()) {
581                         const QString keyToLower(key.toLower());
582                         if (keyToLower.startsWith(Entry::ftUrl) || keyToLower.startsWith(Entry::ftLocalFile) || keyToLower.startsWith(Entry::ftFile) || keyToLower.startsWith(Entry::ftDOI))
583                             /// Filenames and alike have be separated by a semicolon,
584                             /// as a plain comma may be part of the filename or URL
585                             result.append(QStringLiteral("; "));
586                         else
587                             result.append(' ');
588                     } else {
589                         result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter);
590                     }
591                     isOpen = true;
592 
593                     if (d->stringOpenDelimiter == QLatin1Char('"'))
594                         d->protectQuotationMarks(textBody);
595                     result.append(textBody);
596                     prev = verbatimText;
597                 } else {
598                     QSharedPointer<const Person> person = valueItem.dynamicCast<const Person>();
599                     if (!person.isNull()) {
600                         QString firstName = person->firstName();
601                         if (!firstName.isEmpty() && d->requiresPersonQuoting(firstName, false))
602                             firstName = firstName.prepend("{").append("}");
603 
604                         QString lastName = person->lastName();
605                         if (!lastName.isEmpty() && d->requiresPersonQuoting(lastName, true))
606                             lastName = lastName.prepend("{").append("}");
607 
608                         QString suffix = person->suffix();
609 
610                         /// Fall back and enforce comma-based name formatting
611                         /// if name contains a suffix like "Jr."
612                         /// Otherwise name could not be parsed again reliable
613                         const QString pnf = suffix.isEmpty() ? d->personNameFormatting : Preferences::personNameFormatLastFirst;
614                         QString thisName = applyEncoder(Person::transcribePersonName(pnf, firstName, lastName, suffix), useLaTeXEncoding);
615 
616                         if (!isOpen) {
617                             if (!result.isEmpty()) result.append(" # ");
618                             result.append(d->stringOpenDelimiter);
619                         } else if (!prev.dynamicCast<const Person>().isNull())
620                             result.append(" and ");
621                         else {
622                             result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter);
623                         }
624                         isOpen = true;
625 
626                         if (d->stringOpenDelimiter == QLatin1Char('"'))
627                             d->protectQuotationMarks(thisName);
628                         result.append(thisName);
629                         prev = person;
630                     } else {
631                         QSharedPointer<const Keyword> keyword = valueItem.dynamicCast<const Keyword>();
632                         if (!keyword.isNull()) {
633                             QString textBody = applyEncoder(keyword->text(), useLaTeXEncoding);
634                             if (!isOpen) {
635                                 if (!result.isEmpty()) result.append(" # ");
636                                 result.append(d->stringOpenDelimiter);
637                             } else if (!prev.dynamicCast<const Keyword>().isNull())
638                                 result.append(d->listSeparator);
639                             else {
640                                 result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter);
641                             }
642                             isOpen = true;
643 
644                             if (d->stringOpenDelimiter == QLatin1Char('"'))
645                                 d->protectQuotationMarks(textBody);
646                             result.append(textBody);
647                             prev = keyword;
648                         }
649                     }
650                 }
651             }
652         }
653         prev = valueItem;
654     }
655 
656     if (isOpen) result.append(d->stringCloseDelimiter);
657 
658     return result;
659 }
660 
isFileExporterBibTeX(const FileExporter & other)661 bool FileExporterBibTeX::isFileExporterBibTeX(const FileExporter &other) {
662     return typeid(other) == typeid(FileExporterBibTeX);
663 }
664