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("“")); 321 text.replace(QStringLiteral("''"), QStringLiteral("”")); 322 static const QRegularExpression openingSingleQuotationRegExp(QStringLiteral("(^|[> ,.;:!?])`(\\S)")); 323 static const QRegularExpression closingSingleQuotationRegExp(QStringLiteral("(\\S)'([ ,.;:!?<]|$)")); 324 text.replace(openingSingleQuotationRegExp, QStringLiteral("\\1‘\\2")); 325 text.replace(closingSingleQuotationRegExp, QStringLiteral("\\1’\\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