File indexing completed on 2024-05-12 05:10:08

0001 /***************************************************************************
0002     Copyright (C) 2003-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include <config.h>
0026 
0027 #include "bibtexexporter.h"
0028 #include "../collections/bibtexcollection.h"
0029 #include "../core/filehandler.h"
0030 #include "../utils/bibtexhandler.h"
0031 #include "../utils/stringset.h"
0032 #include "../fieldformat.h"
0033 #include "../tellico_debug.h"
0034 
0035 #include <KLocalizedString>
0036 #include <KConfigGroup>
0037 #include <KComboBox>
0038 
0039 #include <QRegularExpression>
0040 #include <QCheckBox>
0041 #include <QGroupBox>
0042 #include <QLabel>
0043 #include <QVBoxLayout>
0044 #include <QHBoxLayout>
0045 
0046 using namespace Tellico;
0047 using Tellico::Export::BibtexExporter;
0048 
0049 BibtexExporter::BibtexExporter(Data::CollPtr coll) : Tellico::Export::Exporter(coll),
0050    m_expandMacros(false),
0051    m_packageURL(true),
0052    m_skipEmptyKeys(false),
0053    m_widget(nullptr),
0054    m_checkExpandMacros(nullptr),
0055    m_checkPackageURL(nullptr),
0056    m_checkSkipEmpty(nullptr),
0057    m_cbBibtexStyle(nullptr) {
0058 }
0059 
0060 QString BibtexExporter::formatString() const {
0061   return QStringLiteral("Bibtex");
0062 }
0063 
0064 QString BibtexExporter::fileFilter() const {
0065   return i18n("Bibtex Files") + QLatin1String(" (*.bib)") + QLatin1String(";;") + i18n("All Files") + QLatin1String(" (*)");
0066 }
0067 
0068 bool BibtexExporter::exec() {
0069   const QString text = this->text();
0070   return text.isEmpty() ? false : FileHandler::writeTextURL(url(), text, options() & ExportUTF8, options() & Export::ExportForce);
0071 }
0072 
0073 QString BibtexExporter::text() {
0074   Data::CollPtr c = collection();
0075   if(!c || c->type() != Data::Collection::Bibtex) {
0076     return QString();
0077   }
0078   const Data::BibtexCollection* coll = static_cast<const Data::BibtexCollection*>(c.data());
0079 
0080 // there are some special attributes
0081 // the entry-type specifies the entry type - book, inproceedings, whatever
0082   QString typeField;
0083 // the key specifies the cite-key
0084   QString keyField;
0085 // the crossref bibtex field can reference another entry
0086   QString crossRefField;
0087   bool hasCrossRefs = false;
0088 
0089   const QString bibtex = QStringLiteral("bibtex");
0090 // keep a list of all the 'ordinary' fields to iterate through later
0091   Data::FieldList fields;
0092   foreach(Data::FieldPtr it, this->fields()) {
0093     QString bibtexField = it->property(bibtex);
0094     if(bibtexField == QLatin1String("entry-type")) {
0095       typeField = it->name();
0096     } else if(bibtexField == QLatin1String("key")) {
0097       keyField = it->name();
0098     } else if(bibtexField == QLatin1String("crossref")) {
0099       fields.append(it); // still output crossref field
0100       crossRefField = it->name();
0101       hasCrossRefs = true;
0102     } else if(!bibtexField.isEmpty()) {
0103       fields.append(it);
0104     }
0105   }
0106 
0107   if(typeField.isEmpty() || keyField.isEmpty()) {
0108     myWarning() << "the collection must have fields defining "
0109                    "the entry-type and the key of the entry";
0110     return QString();
0111   }
0112   if(fields.isEmpty()) {
0113     myWarning() << "no bibtex field mapping exists in the collection.";
0114     return QString();
0115   }
0116 
0117   QString text = QLatin1String("@comment{Generated by Tellico ")
0118                + QLatin1String(TELLICO_VERSION)
0119                + QLatin1String("}\n\n");
0120 
0121   const QStringList macros = coll->macroList().keys();
0122 
0123   if(!coll->preamble().isEmpty()) {
0124     text += QLatin1String("@preamble{")
0125             + BibtexHandler::exportText(coll->preamble(), macros)
0126             + QLatin1String("}\n\n");
0127   }
0128 
0129   if(!m_expandMacros) {
0130     QMap<QString, QString>::ConstIterator macroIt;
0131     for(macroIt = coll->macroList().constBegin(); macroIt != coll->macroList().constEnd(); ++macroIt) {
0132       if(!macroIt.value().isEmpty()) {
0133         text += QLatin1String("@string{")
0134                 + macroIt.key()
0135                 + QLatin1String("=")
0136                 + BibtexHandler::exportText(macroIt.value(), macros)
0137                 + QLatin1String("}\n\n");
0138       }
0139     }
0140   }
0141 
0142   // if anything is crossref'd, we have to do an initial scan through the
0143   // whole collection first
0144   StringSet crossRefKeys;
0145   if(hasCrossRefs) {
0146     foreach(Data::EntryPtr entryIt, entries()) {
0147       crossRefKeys.add(entryIt->field(crossRefField));
0148     }
0149   }
0150 
0151   StringSet usedKeys;
0152   Data::EntryList crossRefs;
0153   QString type, key, newKey;
0154   foreach(Data::EntryPtr entryIt, entries()) {
0155     type = entryIt->field(typeField);
0156     if(type.isEmpty()) {
0157       myWarning() << "the entry for '" << entryIt->title()
0158                   << "' has no entry-type, skipping it!";
0159       continue;
0160     }
0161 
0162     key = entryIt->field(keyField);
0163     if(key.isEmpty()) {
0164       if(m_skipEmptyKeys) {
0165         continue;
0166       }
0167       key = BibtexHandler::bibtexKey(entryIt);
0168     } else {
0169       // check crossrefs, only counts for non-empty keys
0170       // if this entry is crossref'd, add it to the list, and skip it
0171       if(hasCrossRefs && crossRefKeys.has(key)) {
0172         crossRefs.append(entryIt);
0173         continue;
0174       }
0175     }
0176 
0177     newKey = key;
0178     char c = 'a';
0179     while(usedKeys.has(newKey)) {
0180       // duplicate found!
0181       newKey = key + QLatin1Char(c);
0182       ++c;
0183     }
0184     key = newKey;
0185     usedKeys.add(key);
0186 
0187     writeEntryText(text, fields, *entryIt, type, key);
0188   }
0189 
0190   // now write out crossrefs
0191   foreach(Data::EntryPtr entryIt, crossRefs) {
0192     // no need to check type
0193 
0194     key = entryIt->field(keyField);
0195     newKey = key;
0196     char c = 'a';
0197     while(usedKeys.has(newKey)) {
0198       // duplicate found!
0199       newKey = key + QLatin1Char(c);
0200       ++c;
0201     }
0202     key = newKey;
0203     usedKeys.add(key);
0204 
0205     writeEntryText(text, fields, *entryIt, entryIt->field(typeField), key);
0206   }
0207   return text;
0208 }
0209 
0210 QWidget* BibtexExporter::widget(QWidget* parent_) {
0211   if(m_widget && m_widget->parent() == parent_) {
0212     return m_widget;
0213   }
0214 
0215   m_widget = new QWidget(parent_);
0216   QVBoxLayout* l = new QVBoxLayout(m_widget);
0217 
0218   QGroupBox* gbox = new QGroupBox(i18n("Bibtex Options"), m_widget);
0219   QVBoxLayout* vlay = new QVBoxLayout(gbox);
0220 
0221   m_checkExpandMacros = new QCheckBox(i18n("Expand string macros"), gbox);
0222   m_checkExpandMacros->setChecked(m_expandMacros);
0223   m_checkExpandMacros->setWhatsThis(i18n("If checked, the string macros will be expanded and no "
0224                                          "@string{} entries will be written."));
0225 
0226   m_checkPackageURL = new QCheckBox(i18n("Use URL package"), gbox);
0227   m_checkPackageURL->setChecked(m_packageURL);
0228   m_checkPackageURL->setWhatsThis(i18n("If checked, any URL fields will be wrapped in a "
0229                                        "\\url declaration."));
0230 
0231   m_checkSkipEmpty = new QCheckBox(i18n("Skip entries with empty citation keys"), gbox);
0232   m_checkSkipEmpty->setChecked(m_skipEmptyKeys);
0233   m_checkSkipEmpty->setWhatsThis(i18n("If checked, any entries without a bibtex citation key "
0234                                       "will be skipped."));
0235 
0236   QHBoxLayout* hlay = new QHBoxLayout();
0237   vlay->addLayout(hlay);
0238 
0239   QLabel* l1 = new QLabel(i18n("Bibtex quotation style:") + QLatin1Char(' '), gbox); // add a space for aesthetics
0240   m_cbBibtexStyle = new KComboBox(gbox);
0241   m_cbBibtexStyle->addItem(i18n("Braces"));
0242   m_cbBibtexStyle->addItem(i18n("Quotes"));
0243   QString whats = i18n("<qt>The quotation style used when exporting bibtex. All field values will "
0244                        "be escaped with either braces or quotation marks.</qt>");
0245   l1->setWhatsThis(whats);
0246   m_cbBibtexStyle->setWhatsThis(whats);
0247   if(BibtexHandler::s_quoteStyle == BibtexHandler::BRACES) {
0248     m_cbBibtexStyle->setCurrentItem(i18n("Braces"));
0249   } else {
0250     m_cbBibtexStyle->setCurrentItem(i18n("Quotes"));
0251   }
0252 
0253   hlay->addWidget(l1);
0254   hlay->addWidget(m_cbBibtexStyle);
0255 
0256   vlay->addWidget(m_checkExpandMacros);
0257   vlay->addWidget(m_checkPackageURL);
0258   vlay->addWidget(m_checkSkipEmpty);
0259 
0260   l->addWidget(gbox);
0261   l->addStretch(1);
0262   return m_widget;
0263 }
0264 
0265 void BibtexExporter::readOptions(KSharedConfigPtr config_) {
0266   KConfigGroup group(config_, QStringLiteral("ExportOptions - %1").arg(formatString()));
0267   m_expandMacros = group.readEntry("Expand Macros", m_expandMacros);
0268   m_packageURL = group.readEntry("URL Package", m_packageURL);
0269   m_skipEmptyKeys = group.readEntry("Skip Empty Keys", m_skipEmptyKeys);
0270 
0271   if(group.readEntry("Use Braces", true)) {
0272     BibtexHandler::s_quoteStyle = BibtexHandler::BRACES;
0273   } else {
0274     BibtexHandler::s_quoteStyle = BibtexHandler::QUOTES;
0275   }
0276 }
0277 
0278 void BibtexExporter::saveOptions(KSharedConfigPtr config_) {
0279   KConfigGroup group(config_, QStringLiteral("ExportOptions - %1").arg(formatString()));
0280   m_expandMacros = m_checkExpandMacros->isChecked();
0281   group.writeEntry("Expand Macros", m_expandMacros);
0282   m_packageURL = m_checkPackageURL->isChecked();
0283   group.writeEntry("URL Package", m_packageURL);
0284   m_skipEmptyKeys = m_checkSkipEmpty->isChecked();
0285   group.writeEntry("Skip Empty Keys", m_skipEmptyKeys);
0286 
0287   bool useBraces = m_cbBibtexStyle->currentText() == i18n("Braces");
0288   group.writeEntry("Use Braces", useBraces);
0289   if(useBraces) {
0290     BibtexHandler::s_quoteStyle = BibtexHandler::BRACES;
0291   } else {
0292     BibtexHandler::s_quoteStyle = BibtexHandler::QUOTES;
0293   }
0294 }
0295 
0296 void BibtexExporter::writeEntryText(QString& text_, const Tellico::Data::FieldList& fields_, const Tellico::Data::Entry& entry_,
0297                                     const QString& type_, const QString& key_) {
0298   static const QRegularExpression numberRx(QLatin1String("^\\d+$"));
0299   const QStringList macros = static_cast<const Data::BibtexCollection*>(collection().data())->macroList().keys();
0300   const QString bibtex = QStringLiteral("bibtex");
0301   const QString bibtexSep = QStringLiteral("bibtex-separator");
0302 
0303   text_ += QLatin1Char('@') + type_ + QLatin1Char('{') + key_;
0304 
0305   QString value;
0306   FieldFormat::Request format = (options() & Export::ExportFormatted ?
0307                                                 FieldFormat::ForceFormat :
0308                                                 FieldFormat::AsIsFormat);
0309   static const QRegularExpression stripHTML(QLatin1String("<.*?>"));
0310   foreach(Data::FieldPtr fIt, fields_) {
0311     value = entry_.formattedField(fIt, format);
0312     if(value.isEmpty()) {
0313       continue;
0314     }
0315 
0316     // If the entry is formatted as a name and allows multiple values
0317     // insert "and" in between them (e.g. author and editor)
0318     if(fIt->formatType() == FieldFormat::FormatName
0319        && fIt->hasFlag(Data::Field::AllowMultiple)) {
0320       value.replace(FieldFormat::delimiterString(), QLatin1String(" and "));
0321     } else if(fIt->hasFlag(Data::Field::AllowMultiple)) {
0322       QString bibsep = fIt->property(bibtexSep);
0323       if(!bibsep.isEmpty()) {
0324         value.replace(FieldFormat::delimiterString(), bibsep);
0325       }
0326     } else if(fIt->type() == Data::Field::Para) {
0327       // strip HTML from bibtex export
0328       value.remove(stripHTML);
0329     } else if(fIt->property(bibtex) == QLatin1String("pages")) {
0330       static const QRegularExpression rx(QLatin1String("(\\d)-(\\d)"));
0331       QRegularExpressionMatch m = rx.match(value);
0332       for(int pos = m.capturedStart(); pos > -1; pos = m.capturedStart()) {
0333         value.replace(pos, 3, m.captured(1) + QLatin1String("--") + m.captured(2));
0334         m = rx.match(value, pos+2);
0335       }
0336     }
0337 
0338     if(m_packageURL && fIt->type() == Data::Field::URL) {
0339       bool b = BibtexHandler::s_quoteStyle == BibtexHandler::BRACES;
0340       value = (b ? QLatin1Char('{') : QLatin1Char('"'))
0341             + QLatin1String("\\url{") + value + QLatin1Char('}')
0342             + (b ? QLatin1Char('}') : QLatin1Char('"'));
0343     } else if(!numberRx.match(value).hasMatch()) {
0344       // numbers aren't escaped, nor will they have macros
0345       // if m_expandMacros is true, then macros is empty, so this is ok even then
0346       value = BibtexHandler::exportText(value, macros);
0347       // special case for tilde, since it's a non-breaking space in LateX
0348       // replace it EXCEPT for URL or DOI fields
0349       if(fIt->property(bibtex) != QLatin1String("doi") && fIt->type() != Data::Field::URL) {
0350         value.replace(QChar(0xA0), QLatin1Char('~'));
0351       }
0352     }
0353     text_ += QLatin1String(",\n  ")
0354            + fIt->property(bibtex)
0355            + QLatin1String(" = ")
0356            + value;
0357   }
0358   text_ += QLatin1String("\n}\n\n");
0359 }