1 /***************************************************************************
2  *   Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3  *                              and contributors                           *
4  *                                                                         *
5  *   Contributions to this file were made by                               *
6  *   - Jurgen Spitzmuller <juergen@spitzmueller.org>                       *
7  *                                                                         *
8  *   This program is free software; you can redistribute it and/or modify  *
9  *   it under the terms of the GNU General Public License as published by  *
10  *   the Free Software Foundation; either version 2 of the License, or     *
11  *   (at your option) any later version.                                   *
12  *                                                                         *
13  *   This program is distributed in the hope that it will be useful,       *
14  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
15  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
16  *   GNU General Public License for more details.                          *
17  *                                                                         *
18  *   You should have received a copy of the GNU General Public License     *
19  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
20  ***************************************************************************/
21 
22 #include "referencepreview.h"
23 
24 #include <QFrame>
25 #include <QBuffer>
26 #include <QTextDocument>
27 #include <QLayout>
28 #include <QApplication>
29 #include <QTextStream>
30 #include <QTemporaryFile>
31 #include <QPalette>
32 #include <QMimeType>
33 #include <QDebug>
34 #include <QFileDialog>
35 #include <QPushButton>
36 #include <QFontDatabase>
37 
38 #include <KLocalizedString>
39 #include <KComboBox>
40 #include <KRun>
41 #include <KIO/CopyJob>
42 #include <KJobWidgets>
43 #include <KSharedConfig>
44 #include <KConfigGroup>
45 #include <KTextEdit>
46 #include <kio_version.h>
47 
48 #include "xsltransform.h"
49 #include "fileexporterbibtex.h"
50 #include "fileexporterbibtex2html.h"
51 #include "fileexporterris.h"
52 #include "fileexporterxslt.h"
53 #include "element.h"
54 #include "file.h"
55 #include "entry.h"
56 #include "fileview.h"
57 #include "logging_program.h"
58 
59 static const struct PreviewStyles {
60     QString label, style, type;
61 }
62 previewStyles[] = {
63     {i18n("Source (BibTeX)"), QStringLiteral("bibtex"), QStringLiteral("exporter")},
64     {i18n("Source (RIS)"), QStringLiteral("ris"), QStringLiteral("exporter")},
65     {QStringLiteral("abbrv"), QStringLiteral("abbrv"), QStringLiteral("bibtex2html")},
66     {QStringLiteral("acm"), QStringLiteral("acm"), QStringLiteral("bibtex2html")},
67     {QStringLiteral("alpha"), QStringLiteral("alpha"), QStringLiteral("bibtex2html")},
68     {QStringLiteral("apalike"), QStringLiteral("apalike"), QStringLiteral("bibtex2html")},
69     {QStringLiteral("ieeetr"), QStringLiteral("ieeetr"), QStringLiteral("bibtex2html")},
70     {QStringLiteral("plain"), QStringLiteral("plain"), QStringLiteral("bibtex2html")},
71     {QStringLiteral("siam"), QStringLiteral("siam"), QStringLiteral("bibtex2html")},
72     {QStringLiteral("unsrt"), QStringLiteral("unsrt"), QStringLiteral("bibtex2html")},
73     {i18n("Standard"), QStringLiteral("standard"), QStringLiteral("xml")},
74     {i18n("Fancy"), QStringLiteral("fancy"), QStringLiteral("xml")},
75     {i18n("Wikipedia Citation"), QStringLiteral("wikipedia-cite"), QStringLiteral("plain_xml")},
76     {i18n("Abstract-only"), QStringLiteral("abstractonly"), QStringLiteral("xml")}
77 };
78 
79 Q_DECLARE_METATYPE(PreviewStyles)
80 
81 class ReferencePreview::ReferencePreviewPrivate
82 {
83 private:
84     ReferencePreview *p;
85 
86 public:
87     KSharedConfigPtr config;
88     const QString configGroupName;
89     const QString configKeyName;
90 
91     QPushButton *buttonOpen, *buttonSaveAsHTML;
92     QString htmlText;
93     QUrl baseUrl;
94     QTextDocument *htmlDocument;
95     KTextEdit *htmlView;
96     KComboBox *comboBox;
97     QSharedPointer<const Element> element;
98     const File *file;
99     FileView *fileView;
100     const QColor textColor;
101     const int defaultFontSize;
102     const QString htmlStart;
103     const QString notAvailableMessage;
104 
105     ReferencePreviewPrivate(ReferencePreview *parent)
106             : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Reference Preview Docklet")),
107           configKeyName(QStringLiteral("Style")), file(nullptr), fileView(nullptr),
108           textColor(QApplication::palette().text().color()),
109           defaultFontSize(QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize()),
110           htmlStart(QStringLiteral("<html>\n<head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n<style type=\"text/css\">\npre {\n white-space: pre-wrap;\n white-space: -moz-pre-wrap;\n white-space: -pre-wrap;\n white-space: -o-pre-wrap;\n word-wrap: break-word;\n}\n</style>\n</head>\n<body style=\"color: ") + textColor.name() + QStringLiteral("; font-size: ") + QString::number(defaultFontSize) + QStringLiteral("pt; font-family: '") + QFontDatabase::systemFont(QFontDatabase::GeneralFont).family() + QStringLiteral("'; background-color: '") + QApplication::palette().base().color().name(QColor::HexRgb) + QStringLiteral("'\">")),
111           notAvailableMessage(htmlStart + QStringLiteral("<p style=\"font-style: italic;\">") + i18n("No preview available") + QStringLiteral("</p><p style=\"font-size: 90%;\">") + i18n("Reason:") + QStringLiteral(" %1</p></body></html>")) {
112         QGridLayout *gridLayout = new QGridLayout(p);
113         gridLayout->setMargin(0);
114         gridLayout->setColumnStretch(0, 1);
115         gridLayout->setColumnStretch(1, 0);
116         gridLayout->setColumnStretch(2, 0);
117 
118         comboBox = new KComboBox(p);
119         gridLayout->addWidget(comboBox, 0, 0, 1, 3);
120 
121         QFrame *frame = new QFrame(p);
122         gridLayout->addWidget(frame, 1, 0, 1, 3);
123         frame->setFrameShadow(QFrame::Sunken);
124         frame->setFrameShape(QFrame::StyledPanel);
125 
126         QVBoxLayout *layout = new QVBoxLayout(frame);
127         layout->setMargin(0);
128         htmlView = new KTextEdit(frame);
129         htmlView->setReadOnly(true);
130         htmlDocument = new QTextDocument(htmlView);
131         htmlView->setDocument(htmlDocument);
132         layout->addWidget(htmlView);
133 
134         buttonOpen = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open"), p);
135         buttonOpen->setToolTip(i18n("Open reference in web browser."));
136         gridLayout->addWidget(buttonOpen, 2, 1, 1, 1);
137 
138         buttonSaveAsHTML = new QPushButton(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as HTML"), p);
139         buttonSaveAsHTML->setToolTip(i18n("Save reference as HTML fragment."));
140         gridLayout->addWidget(buttonSaveAsHTML, 2, 2, 1, 1);
141     }
142 
143     bool saveHTML(const QUrl &url) const {
144         QTemporaryFile tempFile;
145         tempFile.setAutoRemove(true);
146 
147         bool result = saveHTML(tempFile);
148 
149         if (result) {
150             KIO::CopyJob *copyJob = KIO::copy(QUrl::fromLocalFile(tempFile.fileName()), url, KIO::Overwrite);
151             KJobWidgets::setWindow(copyJob, p);
152             result = copyJob->exec();
153         }
154 
155         return result;
156     }
157 
158     bool saveHTML(QTemporaryFile &tempFile) const {
159         if (tempFile.open()) {
160             QTextStream ts(&tempFile);
161             ts.setCodec("utf-8");
162             static const QRegularExpression kbibtexHrefRegExp(QStringLiteral("<a[^>]+href=\"kbibtex:[^>]+>(.+?)</a>"));
163             QString modifiedHtmlText = htmlText;
164             modifiedHtmlText = modifiedHtmlText.replace(kbibtexHrefRegExp, QStringLiteral("\\1"));
165             ts << modifiedHtmlText;
166             tempFile.close();
167             return true;
168         }
169 
170         return false;
171     }
172 
173     void loadState() {
174         static bool hasBibTeX2HTML = !QStandardPaths::findExecutable(QStringLiteral("bibtex2html")).isEmpty();
175 
176         KConfigGroup configGroup(config, configGroupName);
177         const QString previousStyle = configGroup.readEntry(configKeyName, QString());
178 
179         comboBox->clear();
180 
181         int styleIndex = 0, c = 0;
182         for (const PreviewStyles &previewStyle : previewStyles) {
183             if (!hasBibTeX2HTML && previewStyle.type.contains(QStringLiteral("bibtex2html"))) continue;
184             comboBox->addItem(previewStyle.label, QVariant::fromValue(previewStyle));
185             if (previousStyle == previewStyle.style)
186                 styleIndex = c;
187             ++c;
188         }
189         comboBox->setCurrentIndex(styleIndex);
190     }
191 
192     void saveState() {
193         KConfigGroup configGroup(config, configGroupName);
194         configGroup.writeEntry(configKeyName, comboBox->itemData(comboBox->currentIndex()).value<PreviewStyles>().style);
195         config->sync();
196     }
197 };
198 
199 ReferencePreview::ReferencePreview(QWidget *parent)
200         : QWidget(parent), d(new ReferencePreviewPrivate(this))
201 {
202     d->loadState();
203 
204     connect(d->buttonOpen, &QPushButton::clicked, this, &ReferencePreview::openAsHTML);
205     connect(d->buttonSaveAsHTML, &QPushButton::clicked, this, &ReferencePreview::saveAsHTML);
206     connect(d->comboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &ReferencePreview::renderHTML);
207 
208     setEnabled(false);
209 }
210 
211 ReferencePreview::~ReferencePreview()
212 {
213     delete d;
214 }
215 
216 void ReferencePreview::setHtml(const QString &html, bool buttonsEnabled)
217 {
218     d->htmlText = QString(html).remove(QStringLiteral("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
219     d->htmlDocument->setHtml(d->htmlText);
220     d->buttonOpen->setEnabled(buttonsEnabled);
221     d->buttonSaveAsHTML->setEnabled(buttonsEnabled);
222 }
223 
224 void ReferencePreview::setEnabled(bool enabled)
225 {
226     if (enabled)
227         setHtml(d->htmlText, true);
228     else
229         setHtml(d->notAvailableMessage.arg(i18n("Preview disabled")), false);
230     d->htmlView->setEnabled(enabled);
231     d->comboBox->setEnabled(enabled);
232 }
233 
234 void ReferencePreview::setElement(QSharedPointer<Element> element, const File *file)
235 {
236     d->element = element;
237     d->file = file;
238     renderHTML();
239 }
240 
241 void ReferencePreview::renderHTML()
242 {
243     enum { ignore, /// do not include crossref'ed entry's values (one entry)
244            /// NOT USED: add, /// feed both the current entry as well as the crossref'ed entry into the exporter (two entries)
245            merge /// merge the crossref'ed entry's values into the current entry (one entry)
246          } crossRefHandling = ignore;
247 
248     if (d->element.isNull()) {
249         setHtml(d->notAvailableMessage.arg(i18n("No element selected")), false);
250         return;
251     }
252 
253     QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
254 
255     FileExporter *exporter = nullptr;
256 
257     const PreviewStyles previewStyle = d->comboBox->itemData(d->comboBox->currentIndex()).value<PreviewStyles>();
258 
259     if (previewStyle.type == QStringLiteral("exporter")) {
260         if (previewStyle.style == QStringLiteral("bibtex")) {
261             FileExporterBibTeX *exporterBibTeX = new FileExporterBibTeX(this);
262             exporterBibTeX->setEncoding(QStringLiteral("utf-8"));
263             exporter = exporterBibTeX;
264         } else if (previewStyle.style == QStringLiteral("ris"))
265             exporter = new FileExporterRIS(this);
266         else
267             qCWarning(LOG_KBIBTEX_PROGRAM) << "Don't know how to handle output style " << previewStyle.style << " for type " << previewStyle.type;
268     } else if (previewStyle.type == QStringLiteral("bibtex2html")) {
269         crossRefHandling = merge;
270         FileExporterBibTeX2HTML *exporterHTML = new FileExporterBibTeX2HTML(this);
271         exporterHTML->setLaTeXBibliographyStyle(previewStyle.style);
272         exporter = exporterHTML;
273     } else if (previewStyle.type == QStringLiteral("xml") || previewStyle.type.endsWith(QStringLiteral("_xml"))) {
274         crossRefHandling = merge;
275         const QString filename = previewStyle.style + QStringLiteral(".xsl");
276         exporter = new FileExporterXSLT(XSLTransform::locateXSLTfile(filename), this);
277     } else
278         qCWarning(LOG_KBIBTEX_PROGRAM) << "Don't know how to handle output type " << previewStyle.type;
279 
280     if (exporter != nullptr) {
281         QBuffer buffer(this);
282         buffer.open(QBuffer::WriteOnly);
283 
284         bool exporterResult = false;
285         QStringList errorLog;
286         QSharedPointer<const Entry> entry = d->element.dynamicCast<const Entry>();
287         /** NOT USED
288         if (crossRefHandling == add && !entry.isNull()) {
289             QString crossRef = PlainTextValue::text(entry->value(QStringLiteral("crossref")));
290             QSharedPointer<const Entry> crossRefEntry = d->file == NULL ? QSharedPointer<const Entry>() : d->file->containsKey(crossRef) .dynamicCast<const Entry>();
291             if (!crossRefEntry.isNull()) {
292                 File file;
293                 file.append(QSharedPointer<Entry>(new Entry(*entry)));
294                 file.append(QSharedPointer<Entry>(new Entry(*crossRefEntry)));
295                 exporterResult = exporter->save(&buffer, &file, &errorLog);
296             } else
297                 exporterResult = exporter->save(&buffer, d->element, d->file, &errorLog);
298         } else */
299         if (crossRefHandling == merge && !entry.isNull()) {
300             QSharedPointer<Entry> merged = QSharedPointer<Entry>(entry->resolveCrossref(d->file));
301             exporterResult = exporter->save(&buffer, merged, d->file, &errorLog);
302         } else
303             exporterResult = exporter->save(&buffer, d->element, d->file, &errorLog);
304         buffer.close();
305         delete exporter;
306 
307         buffer.open(QBuffer::ReadOnly);
308         QString text = QString::fromUtf8(buffer.readAll().constData());
309         buffer.close();
310 
311         bool buttonsEnabled = true;
312 
313         if (!exporterResult || text.isEmpty()) {
314             /// something went wrong, no output ...
315             text = d->notAvailableMessage.arg(i18n("No output generated"));
316             buttonsEnabled = false;
317             qCDebug(LOG_KBIBTEX_PROGRAM) << errorLog.join(QStringLiteral("\n"));
318         } else {
319             /// beautify text
320             text.replace(QStringLiteral("``"), QStringLiteral("&ldquo;"));
321             text.replace(QStringLiteral("''"), QStringLiteral("&rdquo;"));
322             static const QRegularExpression openingSingleQuotationRegExp(QStringLiteral("(^|[> ,.;:!?])`(\\S)"));
323             static const QRegularExpression closingSingleQuotationRegExp(QStringLiteral("(\\S)'([ ,.;:!?<]|$)"));
324             text.replace(openingSingleQuotationRegExp, QStringLiteral("\\1&lsquo;\\2"));
325             text.replace(closingSingleQuotationRegExp, QStringLiteral("\\1&rsquo;\\2"));
326 
327             if (previewStyle.style == QStringLiteral("wikipedia-cite"))
328                 text.remove(QStringLiteral("\n"));
329 
330             if (text.contains(QStringLiteral("{{cite FIXME"))) {
331                 /// Wikipedia {{cite ...}} command had problems (e.g. unknown entry type)
332                 text = d->notAvailableMessage.arg(i18n("This type of element is not supported by Wikipedia's <tt>{{cite}}</tt> command."));
333             } else if (previewStyle.type == QStringLiteral("exporter") || previewStyle.type.startsWith(QStringLiteral("plain_"))) {
334                 /// source
335                 text.prepend(QStringLiteral("';\">"));
336                 text.prepend(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
337                 text.prepend(QStringLiteral("<pre style=\"font-family: '"));
338                 text.prepend(d->htmlStart);
339                 text.append(QStringLiteral("</pre></body></html>"));
340             } else if (previewStyle.type == QStringLiteral("bibtex2html")) {
341                 /// bibtex2html
342 
343                 /// remove "generated by" line from HTML code if BibTeX2HTML was used
344                 text.remove(QRegularExpression(QStringLiteral("<hr><p><em>.*</p>")));
345                 text.remove(QRegularExpression(QStringLiteral("<[/]?(font)[^>]*>")));
346                 text.remove(QRegularExpression(QStringLiteral("^.*?<td.*?</td.*?<td>")));
347                 text.remove(QRegularExpression(QStringLiteral("</td>.*$")));
348                 text.remove(QRegularExpression(QStringLiteral("\\[ <a.*?</a> \\]")));
349 
350                 /// replace ASCII art with Unicode characters
351                 text.replace(QStringLiteral("---"), QString(QChar(0x2014)));
352                 text.replace(QStringLiteral("--"), QString(QChar(0x2013)));
353 
354                 text.prepend(d->htmlStart);
355                 text.append("</body></html>");
356             } else if (previewStyle.type == QStringLiteral("xml")) {
357                 /// XML/XSLT
358                 text.prepend(d->htmlStart);
359                 text.append("</body></html>");
360             }
361 
362             /// adopt current color scheme
363             text.replace(QStringLiteral("color: black;"), QString(QStringLiteral("color: %1;")).arg(d->textColor.name()));
364         }
365 
366         setHtml(text, buttonsEnabled);
367 
368         d->saveState();
369     } else {
370         /// something went wrong, no exporter ...
371         setHtml(d->notAvailableMessage.arg(i18n("No output generated")), false);
372     }
373 
374     QApplication::restoreOverrideCursor();
375 }
376 
377 void ReferencePreview::openAsHTML()
378 {
379     QTemporaryFile file(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + QStringLiteral("referencePreview-openAsHTML-XXXXXX.html"));
380     file.setAutoRemove(false); /// let file stay alive for browser
381     d->saveHTML(file);
382 
383     /// Ask KDE subsystem to open url in viewer matching mime type
384     QUrl url(file.fileName());
385 #if KIO_VERSION < 0x051f00 // < 5.31.0
386     KRun::runUrl(url, QStringLiteral("text/html"), this, false, false);
387 #else // KIO_VERSION < 0x051f00 // >= 5.31.0
388     KRun::runUrl(url, QStringLiteral("text/html"), this, KRun::RunFlags());
389 #endif // KIO_VERSION < 0x051f00
390 }
391 
392 void ReferencePreview::saveAsHTML()
393 {
394     QUrl url = QFileDialog::getSaveFileUrl(this, i18n("Save as HTML"), QUrl(), QStringLiteral("text/html"));
395     if (url.isValid())
396         d->saveHTML(url);
397 }
398 
399 void ReferencePreview::linkClicked(const QUrl &url)
400 {
401     QString text = url.toDisplayString();
402     if (text.startsWith(QStringLiteral("kbibtex:filter:"))) {
403         text = text.mid(15);
404         if (d->fileView != nullptr) {
405             int p = text.indexOf(QStringLiteral("="));
406             SortFilterFileModel::FilterQuery fq;
407             fq.terms << text.mid(p + 1);
408             fq.combination = SortFilterFileModel::EveryTerm;
409             fq.field = text.left(p);
410             fq.searchPDFfiles = false;
411             d->fileView->setFilterBarFilter(fq);
412         }
413     }
414 }
415 
416 void ReferencePreview::setFileView(FileView *fileView)
417 {
418     d->fileView = fileView;
419 }
420