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