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-2019 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 "checkbibtex.h"
0021 
0022 #include <QApplication>
0023 #include <QBuffer>
0024 #include <QTextStream>
0025 #include <QRegularExpression>
0026 
0027 #include <KLocalizedString>
0028 #include <KMessageBox>
0029 
0030 #include <File>
0031 #include <Entry>
0032 #include <Element>
0033 #include <Macro>
0034 #include <FileExporterBibTeXOutput>
0035 
0036 CheckBibTeX::CheckBibTeXResult CheckBibTeX::checkBibTeX(QSharedPointer<Element> &element, const File *file, QWidget *parent)
0037 {
0038     /// only entries are supported, no macros, preambles, ...
0039     QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
0040     if (entry.isNull())
0041         return CheckBibTeXResult::InvalidData;
0042     else
0043         return checkBibTeX(entry, file, parent);
0044 }
0045 
0046 CheckBibTeX::CheckBibTeXResult CheckBibTeX::checkBibTeX(QSharedPointer<Entry> &entry, const File *file, QWidget *parent)
0047 {
0048     /// disable GUI under process
0049     QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
0050 
0051     /// use a dummy BibTeX file to collect all elements necessary for check
0052     File dummyFile;
0053 
0054     /// create temporary entry to work with
0055     dummyFile << entry;
0056 
0057     /// fetch and inser crossref'ed entry
0058     QString crossRefStr;
0059     Value crossRefVal = entry->value(Entry::ftCrossRef);
0060     if (!crossRefVal.isEmpty() && file != nullptr) {
0061         crossRefStr = PlainTextValue::text(crossRefVal);
0062         QSharedPointer<Entry> crossRefDest = file->containsKey(crossRefStr, File::ElementType::Entry).dynamicCast<Entry>();
0063         if (!crossRefDest.isNull())
0064             dummyFile << crossRefDest;
0065         else
0066             crossRefStr.clear(); /// memorize crossref'ing failed
0067     }
0068 
0069     /// include all macro definitions, in case they are referenced
0070     if (file != nullptr)
0071         for (const auto &element : const_cast<const File &>(*file))
0072             if (Macro::isMacro(*element))
0073                 dummyFile << element;
0074 
0075     /// run special exporter to get BibTeX's output
0076     QStringList bibtexOutput;
0077     QByteArray ba;
0078     QBuffer buffer(&ba);
0079     buffer.open(QIODevice::WriteOnly);
0080     FileExporterBibTeXOutput exporter(FileExporterBibTeXOutput::OutputType::BibTeXLogFile, parent);
0081     QObject::connect(&exporter, &FileExporterBibTeXOutput::processStandardOut, parent, [&bibtexOutput](const QString & line) {
0082         bibtexOutput.append(line);
0083     });
0084     QObject::connect(&exporter, &FileExporterBibTeXOutput::processStandardError, parent, [&bibtexOutput](const QString & line) {
0085         bibtexOutput.append(line);
0086     });
0087     bool exporterResult = exporter.save(&buffer, &dummyFile);
0088     buffer.close();
0089 
0090     if (!exporterResult) {
0091         QApplication::restoreOverrideCursor();
0092         KMessageBox::errorList(parent, i18n("Running BibTeX failed.\n\nSee the following output to trace the error:"), bibtexOutput, i18n("Running BibTeX failed."));
0093         return CheckBibTeXResult::FailedToCheck;
0094     }
0095 
0096     /// define variables how to parse BibTeX's output
0097     static const QString warningStart = QStringLiteral("Warning--");
0098     static const QRegularExpression warningEmptyField(QStringLiteral("empty (\\w+) in "));
0099     static const QRegularExpression warningEmptyField2(QStringLiteral("empty (\\w+) or (\\w+) in "));
0100     static const QRegularExpression warningThereIsBut(QStringLiteral("there's a (\\w+) but no (\\w+) in"));
0101     static const QRegularExpression warningCantUseBoth(QStringLiteral("can't use both (\\w+) and (\\w+) fields"));
0102     static const QRegularExpression warningSort2(QStringLiteral("to sort, need (\\w+) or (\\w+) in "));
0103     static const QRegularExpression warningSort3(QStringLiteral("to sort, need (\\w+), (\\w+), or (\\w+) in "));
0104     static const QRegularExpression errorLine(QStringLiteral("---line (\\d+)"));
0105 
0106     /// go line-by-line through BibTeX output and collect warnings/errors
0107     QStringList warnings;
0108     QString errorPlainText;
0109     for (const QString &line : const_cast<const QStringList &>(bibtexOutput)) {
0110         QRegularExpressionMatch match;
0111         if ((match = errorLine.match(line)).hasMatch()) {
0112             buffer.open(QIODevice::ReadOnly);
0113             QTextStream ts(&buffer);
0114             bool ok = false;
0115             for (int i = match.captured(1).toInt(&ok); ok && i > 1; --i) {
0116                 errorPlainText = ts.readLine();
0117                 buffer.close();
0118             }
0119         } else if (line.startsWith(QStringLiteral("Warning--"))) {
0120             /// is a warning ...
0121 
0122             if ((match = warningEmptyField.match(line)).hasMatch()) {
0123                 /// empty/missing field
0124                 warnings << i18n("Field <b>%1</b> is empty", match.captured(1));
0125             } else if ((match = warningEmptyField2.match(line)).hasMatch()) {
0126                 /// two empty/missing fields
0127                 warnings << i18n("Fields <b>%1</b> and <b>%2</b> are empty, but at least one is required", match.captured(1), match.captured(2));
0128             } else if ((match = warningThereIsBut.match(line)).hasMatch()) {
0129                 /// there is a field which exists but another does not exist
0130                 warnings << i18n("Field <b>%1</b> exists, but <b>%2</b> does not exist", match.captured(1), match.captured(2));
0131             } else if ((match = warningCantUseBoth.match(line)).hasMatch()) {
0132                 /// there are two conflicting fields, only one may be used
0133                 warnings << i18n("Fields <b>%1</b> and <b>%2</b> cannot be used at the same time", match.captured(1), match.captured(2));
0134             } else if ((match = warningSort2.match(line)).hasMatch()) {
0135                 /// one out of two fields missing for sorting
0136                 warnings << i18n("Fields <b>%1</b> or <b>%2</b> are required to sort entry", match.captured(1), match.captured(2));
0137             } else if ((match = warningSort3.match(line)).hasMatch()) {
0138                 /// one out of three fields missing for sorting
0139                 warnings << i18n("Fields <b>%1</b>, <b>%2</b>, <b>%3</b> are required to sort entry", match.captured(1), match.captured(2), match.captured(3));
0140             } else {
0141                 /// generic/unknown warning
0142                 warnings << i18n("Unknown warning: %1", line.mid(warningStart.length()));
0143             }
0144         }
0145     }
0146 
0147     CheckBibTeXResult result = CheckBibTeXResult::NoProblem;
0148     QApplication::restoreOverrideCursor();
0149     if (!errorPlainText.isEmpty()) {
0150         result = CheckBibTeXResult::BibTeXWarning;
0151         KMessageBox::information(parent, i18n("<qt><p>The following error was found:</p><pre>%1</pre></qt>", errorPlainText), i18n("Errors found"));
0152     } else if (!warnings.isEmpty()) {
0153         KMessageBox::informationList(parent, i18n("The following warnings were found:"), warnings, i18n("Warnings found"));
0154         result = CheckBibTeXResult::BibTeXError;
0155     } else
0156         KMessageBox::information(parent, i18n("No warnings or errors were found.%1", crossRefStr.isEmpty() ? QString() : i18n("\n\nSome fields missing in this entry were taken from the crossref'ed entry '%1'.", crossRefStr)), i18n("No Errors or Warnings"));
0157 
0158     return result;
0159 }