diff --git a/src/gui/element/elementwidgets.cpp b/src/gui/element/elementwidgets.cpp index 9f8ec3d0..aa7acfc5 100644 --- a/src/gui/element/elementwidgets.cpp +++ b/src/gui/element/elementwidgets.cpp @@ -1,1380 +1,1380 @@ /*************************************************************************** * Copyright (C) 2004-2018 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "elementwidgets.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "preferences.h" #include "idsuggestions.h" #include "fileinfo.h" #include "kbibtex.h" #include "bibtexentries.h" #include "bibtexfields.h" #include "fileimporterbibtex.h" #include "fileexporterbibtex.h" #include "file.h" #include "fieldinput.h" #include "entry.h" #include "macro.h" #include "preamble.h" #include "fieldlineedit.h" #include "delayedexecutiontimer.h" #include "logging_gui.h" static const unsigned int interColumnSpace = 16; static const char *PropertyIdSuggestion = "PropertyIdSuggestion"; ElementWidget::ElementWidget(QWidget *parent) : QWidget(parent), isReadOnly(false), m_file(nullptr), m_isModified(false) { /// nothing } bool ElementWidget::isModified() const { return m_isModified; } void ElementWidget::setModified(bool newIsModified) { m_isModified = newIsModified; emit modified(newIsModified); } void ElementWidget::gotModified() { setModified(true); } EntryConfiguredWidget::EntryConfiguredWidget(const QSharedPointer &entryTabLayout, QWidget *parent) : ElementWidget(parent), fieldInputCount(entryTabLayout->singleFieldLayouts.size()), numCols(entryTabLayout->columns), etl(entryTabLayout) { gridLayout = new QGridLayout(this); /// Initialize list of field input widgets plus labels listOfLabeledFieldInput = new LabeledFieldInput*[fieldInputCount]; createGUI(); } EntryConfiguredWidget::~EntryConfiguredWidget() { delete[] listOfLabeledFieldInput; } bool EntryConfiguredWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; for (QMap::ConstIterator it = bibtexKeyToWidget.constBegin(); it != bibtexKeyToWidget.constEnd(); ++it) { Value value; it.value()->apply(value); entry->remove(it.key()); if (!value.isEmpty()) entry->insert(it.key(), value); } return true; } bool EntryConfiguredWidget::reset(QSharedPointer element) { QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; /// clear all widgets for (QMap::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) { it.value()->setFile(m_file); it.value()->clear(); } for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) { const QString key = it.key().toLower(); if (bibtexKeyToWidget.contains(key)) { FieldInput *fieldInput = bibtexKeyToWidget[key]; fieldInput->setElement(element.data()); fieldInput->reset(it.value()); } } return true; } bool EntryConfiguredWidget::validate(QWidget **widgetWithIssue, QString &message) const { for (int i = fieldInputCount - 1; i >= 0; --i) { const bool v = listOfLabeledFieldInput[i]->fieldInput->validate(widgetWithIssue, message); if (!v) return false; } return true; } void EntryConfiguredWidget::showReqOptWidgets(bool forceVisible, const QString &entryType) { layoutGUI(forceVisible, entryType); } void EntryConfiguredWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); for (QMap::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) it.value()->setReadOnly(isReadOnly); } QString EntryConfiguredWidget::label() { return etl->uiCaption; } QIcon EntryConfiguredWidget::icon() { return QIcon::fromTheme(etl->iconName); } void EntryConfiguredWidget::setFile(const File *file) { for (QMap::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) { it.value()->setFile(file); if (file != nullptr) { /// list of unique values for same field QStringList list = file->uniqueEntryValuesList(it.key()); /// for crossref fields, add all entries' ids if (it.key().toLower() == Entry::ftCrossRef) list.append(file->allKeys(File::etEntry)); /// add macro keys list.append(file->allKeys(File::etMacro)); it.value()->setCompletionItems(list); } } ElementWidget::setFile(file); } bool EntryConfiguredWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } void EntryConfiguredWidget::createGUI() { int i = 0; for (const SingleFieldLayout &sfl : const_cast &>(etl->singleFieldLayouts)) { LabeledFieldInput *labeledFieldInput = new LabeledFieldInput; /// create an editing widget for this field const FieldDescription &fd = BibTeXFields::instance().find(sfl.bibtexLabel); labeledFieldInput->fieldInput = new FieldInput(sfl.fieldInputLayout, fd.preferredTypeFlag, fd.typeFlags, this); labeledFieldInput->fieldInput->setFieldKey(sfl.bibtexLabel); bibtexKeyToWidget.insert(sfl.bibtexLabel, labeledFieldInput->fieldInput); connect(labeledFieldInput->fieldInput, &FieldInput::modified, this, &EntryConfiguredWidget::gotModified); /// memorize if field input should grow vertically (e.g. is a list) labeledFieldInput->isVerticallyMinimumExpaning = sfl.fieldInputLayout == KBibTeX::MultiLine || sfl.fieldInputLayout == KBibTeX::List || sfl.fieldInputLayout == KBibTeX::PersonList || sfl.fieldInputLayout == KBibTeX::KeywordList; /// create a label next to the editing widget labeledFieldInput->label = new QLabel(QString(QStringLiteral("%1:")).arg(sfl.uiLabel), this); labeledFieldInput->label->setBuddy(labeledFieldInput->fieldInput->buddy()); /// align label's text vertically to match field input const Qt::Alignment horizontalAlignment = static_cast(labeledFieldInput->label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)) & Qt::AlignHorizontal_Mask; labeledFieldInput->label->setAlignment(horizontalAlignment | (labeledFieldInput->isVerticallyMinimumExpaning ? Qt::AlignTop : Qt::AlignVCenter)); listOfLabeledFieldInput[i] = labeledFieldInput; ++i; } layoutGUI(true); } void EntryConfiguredWidget::layoutGUI(bool forceVisible, const QString &entryType) { QStringList visibleItems; if (!forceVisible && !entryType.isEmpty()) { const QString entryTypeLc = entryType.toLower(); for (const auto &ed : BibTeXEntries::instance()) { if (entryTypeLc == ed.upperCamelCase.toLower() || entryTypeLc == ed.upperCamelCaseAlt.toLower()) { /// this ugly conversion is necessary because we have a "^" (xor) and "|" (and/or) /// syntax to differentiate required items (not used yet, but will be used /// later if missing required items are marked). QString visible = ed.requiredItems.join(QStringLiteral(",")); visible += QLatin1Char(',') + ed.optionalItems.join(QStringLiteral(",")); visible = visible.replace(QLatin1Char('|'), QLatin1Char(',')).replace(QLatin1Char('^'), QLatin1Char(',')); visibleItems = visible.split(QStringLiteral(",")); break; } } } else if (!forceVisible) { // TODO is this an error condition? } /// variables to keep track which and how many field inputs will be visible int countVisible = 0; QScopedArrayPointer visible(new bool[fieldInputCount]); /// ... and if any field input is vertically expaning /// (e.g. a list, important for layout) bool anyoneVerticallyExpanding = false; for (int i = fieldInputCount - 1; i >= 0; --i) { listOfLabeledFieldInput[i]->label->setVisible(false); listOfLabeledFieldInput[i]->fieldInput->setVisible(false); gridLayout->removeWidget(listOfLabeledFieldInput[i]->label); gridLayout->removeWidget(listOfLabeledFieldInput[i]->fieldInput); const QString key = bibtexKeyToWidget.key(listOfLabeledFieldInput[i]->fieldInput).toLower(); const FieldDescription &fd = BibTeXFields::instance().find(key); Value value; listOfLabeledFieldInput[i]->fieldInput->apply(value); /// Hide non-required and non-optional type-dependent fields, /// except if the field has content visible[i] = forceVisible || fd.typeIndependent || !value.isEmpty() || visibleItems.contains(key); if (visible[i]) { ++countVisible; anyoneVerticallyExpanding |= listOfLabeledFieldInput[i]->isVerticallyMinimumExpaning; } } int numRows = countVisible / numCols; if (countVisible % numCols > 0) ++numRows; gridLayout->setRowStretch(numRows, anyoneVerticallyExpanding ? 0 : 1000); int col = 0, row = 0; for (int i = 0; i < fieldInputCount; ++i) if (visible[i]) { /// add label and field input to new position in grid layout gridLayout->addWidget(listOfLabeledFieldInput[i]->label, row, col * 3); gridLayout->addWidget(listOfLabeledFieldInput[i]->fieldInput, row, col * 3 + 1); /// set row stretch gridLayout->setRowStretch(row, listOfLabeledFieldInput[i]->isVerticallyMinimumExpaning ? 1000 : 0); /// set column stretch and spacing gridLayout->setColumnStretch(col * 3, 1); gridLayout->setColumnStretch(col * 3 + 1, 1000); if (col > 0) gridLayout->setColumnMinimumWidth(col * 3 - 1, interColumnSpace); /// count rows and columns correctly ++row; if (row >= numRows) { row = 0; ++col; } /// finally, set label and field input visible again listOfLabeledFieldInput[i]->label->setVisible(true); listOfLabeledFieldInput[i]->fieldInput->setVisible(true); // FIXME expensive! } if (countVisible > 0) { /// fix row stretch for (int i = numRows + 1; i < 100; ++i) gridLayout->setRowStretch(i, 0); /// hide unused columns for (int i = (col + (row == 0 ? 0 : 1)) * 3 - 1; i < 100; ++i) { gridLayout->setColumnMinimumWidth(i, 0); gridLayout->setColumnStretch(i, 0); } } } ReferenceWidget::ReferenceWidget(QWidget *parent) : ElementWidget(parent), m_applyElement(nullptr), m_entryIdManuallySet(false), m_element(QSharedPointer()) { createGUI(); } bool ReferenceWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode bool result = false; QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) { entry->setType(computeType()); entry->setId(entryId->text()); result = true; } else { QSharedPointer macro = element.dynamicCast(); if (!macro.isNull()) { macro->setKey(entryId->text()); result = true; } } return result; } bool ReferenceWidget::reset(QSharedPointer element) { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widgets' values disconnect(entryType->lineEdit(), &KLineEdit::textChanged, this, &ReferenceWidget::gotModified); disconnect(entryId, &KLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); bool result = false; QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) { entryType->setEnabled(!isReadOnly); buttonSuggestId->setEnabled(!isReadOnly); QString type = BibTeXEntries::instance().format(entry->type(), KBibTeX::cUpperCamelCase); int index = entryType->findData(type); if (index == -1) { const QString typeLower(type.toLower()); for (const auto &ed : BibTeXEntries::instance()) if (typeLower == ed.upperCamelCaseAlt.toLower()) { index = entryType->findData(ed.upperCamelCase); break; } } entryType->setCurrentIndex(index); if (index == -1) { /// A customized value not known to KBibTeX entryType->lineEdit()->setText(type); } entryId->setText(entry->id()); /// New entries have no values. Use this fact /// to recognize new entries, for which it is /// allowed to automatic set their ids /// if a default id suggestion had been specified. m_entryIdManuallySet = entry->count() > 0; result = true; } else { entryType->setEnabled(false); buttonSuggestId->setEnabled(false); QSharedPointer macro = element.dynamicCast(); if (!macro.isNull()) { entryType->lineEdit()->setText(i18n("Macro")); entryId->setText(macro->key()); result = true; } } connect(entryId, &KLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); connect(entryType->lineEdit(), &KLineEdit::textChanged, this, &ReferenceWidget::gotModified); return result; } bool ReferenceWidget::validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); static const QRegularExpression validTypeRegExp(QStringLiteral("^[a-z]+$"), QRegularExpression::CaseInsensitiveOption); const QString type = computeType(); const QRegularExpressionMatch validTypeMatch = validTypeRegExp.match(type); if (!validTypeMatch.hasMatch() || validTypeMatch.capturedLength() != type.length()) { if (widgetWithIssue != nullptr) *widgetWithIssue = entryType; message = i18n("Element type '%1' is invalid.", type); return false; } static const QRegularExpression validIdRegExp(QStringLiteral("^[a-z0-9][a-z0-9_:.+/$\\\"&-]*$"), QRegularExpression::CaseInsensitiveOption); const QString id = entryId->text(); const QRegularExpressionMatch validIdMatch = validIdRegExp.match(id); if (!validIdMatch.hasMatch() || validIdMatch.capturedLength() != id.length()) { if (widgetWithIssue != nullptr) *widgetWithIssue = entryId; message = i18n("Id '%1' is invalid", id); return false; } return true; } void ReferenceWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); entryId->setReadOnly(isReadOnly); entryType->setEnabled(!isReadOnly); } QString ReferenceWidget::label() { return QString(); } QIcon ReferenceWidget::icon() { return QIcon(); } bool ReferenceWidget::canEdit(const Element *element) { return Entry::isEntry(*element) || Macro::isMacro(*element); } void ReferenceWidget::setOriginalElement(const QSharedPointer &orig) { m_element = orig; } QString ReferenceWidget::currentId() const { return entryId->text(); } void ReferenceWidget::setCurrentId(const QString &newId) { entryId->setText(newId); } void ReferenceWidget::createGUI() { QHBoxLayout *layout = new QHBoxLayout(this); entryType = new KComboBox(this); entryType->setEditable(true); entryType->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); QLabel *label = new QLabel(i18n("Type:"), this); label->setBuddy(entryType); layout->addWidget(label); layout->addWidget(entryType); layout->addSpacing(interColumnSpace); entryId = new KLineEdit(this); entryId->setClearButtonEnabled(true); label = new QLabel(i18n("Id:"), this); label->setBuddy(entryId); layout->addWidget(label); layout->addWidget(entryId); for (const auto &ed : BibTeXEntries::instance()) entryType->addItem(ed.label, ed.upperCamelCase); /// Sort the combo box locale-aware. Thus we need a SortFilterProxyModel QSortFilterProxyModel *proxy = new QSortFilterProxyModel(entryType); proxy->setSortLocaleAware(true); proxy->setSourceModel(entryType->model()); entryType->model()->setParent(proxy); entryType->setModel(proxy); entryType->model()->sort(0); /// Button with a menu listing a set of preconfigured id suggestions buttonSuggestId = new QPushButton(QIcon::fromTheme(QStringLiteral("view-filter")), QString(), this); buttonSuggestId->setToolTip(i18n("Select a suggested id for this entry")); layout->addWidget(buttonSuggestId); QMenu *suggestionsMenu = new QMenu(buttonSuggestId); buttonSuggestId->setMenu(suggestionsMenu); connect(entryType->lineEdit(), &KLineEdit::textChanged, this, &ReferenceWidget::gotModified); connect(entryId, &KLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); connect(entryType->lineEdit(), &KLineEdit::textChanged, this, &ReferenceWidget::entryTypeChanged); connect(suggestionsMenu, &QMenu::aboutToShow, this, &ReferenceWidget::prepareSuggestionsMenu); } void ReferenceWidget::prepareSuggestionsMenu() { /// Collect information on the current entry as it is edited QSharedPointer guiDataEntry(new Entry()); m_applyElement->apply(guiDataEntry); QSharedPointer crossrefResolvedEntry(guiDataEntry->resolveCrossref(m_file)); static const IdSuggestions *idSuggestions = new IdSuggestions(); QMenu *suggestionsMenu = buttonSuggestId->menu(); suggestionsMenu->clear(); /// Keep track of shown suggestions to avoid duplicates QSet knownIdSuggestion; const QString defaultSuggestion = idSuggestions->defaultFormatId(*crossrefResolvedEntry.data()); const auto formatIdList = idSuggestions->formatIdList(*crossrefResolvedEntry.data()); for (const QString &suggestionBase : formatIdList) { bool isDefault = suggestionBase == defaultSuggestion; QString suggestion = suggestionBase; /// Test for duplicate ids, use fallback ids with numeric suffix if (m_file != nullptr && m_file->containsKey(suggestion)) { int suffix = 2; while (m_file->containsKey(suggestion = suggestionBase + QChar('_') + QString::number(suffix))) ++suffix; } /// Keep track of shown suggestions to avoid duplicates if (knownIdSuggestion.contains(suggestion)) continue; else knownIdSuggestion.insert(suggestion); /// Create action for suggestion, use icon depending if default or not QAction *suggestionAction = new QAction(suggestion, suggestionsMenu); suggestionAction->setIcon(QIcon::fromTheme(isDefault ? QStringLiteral("favorites") : QStringLiteral("view-filter"))); /// Mesh action into GUI suggestionsMenu->addAction(suggestionAction); connect(suggestionAction, &QAction::triggered, this, &ReferenceWidget::insertSuggestionFromAction); /// Remember suggestion string for time when action gets triggered suggestionAction->setProperty(PropertyIdSuggestion, suggestion); } } void ReferenceWidget::insertSuggestionFromAction() { QAction *action = qobject_cast(sender()); if (action != nullptr) { const QString suggestion = action->property(PropertyIdSuggestion).toString(); entryId->setText(suggestion); } } void ReferenceWidget::entryIdManuallyChanged() { m_entryIdManuallySet = true; gotModified(); } void ReferenceWidget::setEntryIdByDefault() { if (isReadOnly) { /// Never set the suggestion automatically if in read-only mode return; } if (m_entryIdManuallySet) { /// If user changed entry id manually, /// do not overwrite it by a default value return; } static const IdSuggestions *idSuggestions = new IdSuggestions(); /// If there is a default suggestion format set ... if (idSuggestions->hasDefaultFormat()) { /// Collect information on the current entry as it is edited QSharedPointer guiDataEntry(new Entry()); m_applyElement->apply(guiDataEntry); QSharedPointer crossrefResolvedEntry(guiDataEntry->resolveCrossref(m_file)); /// Determine default suggestion based on current data const QString defaultSuggestion = idSuggestions->defaultFormatId(*crossrefResolvedEntry.data()); if (!defaultSuggestion.isEmpty()) { disconnect(entryId, &KLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); /// Apply default suggestion to widget entryId->setText(defaultSuggestion); connect(entryId, &KLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); } } } QString ReferenceWidget::computeType() const { if (entryType->currentIndex() < 0 || entryType->lineEdit()->isModified()) return BibTeXEntries::instance().format(entryType->lineEdit()->text(), KBibTeX::cUpperCamelCase); else return entryType->itemData(entryType->currentIndex()).toString(); } FilesWidget::FilesWidget(QWidget *parent) : ElementWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); fileList = new FieldInput(KBibTeX::UrlList, KBibTeX::tfVerbatim /* eventually ignored, see constructor of UrlListEdit */, KBibTeX::tfVerbatim /* eventually ignored, see constructor of UrlListEdit */, this); fileList->setFieldKey(QStringLiteral("^external")); layout->addWidget(fileList); connect(fileList, &FieldInput::modified, this, &FilesWidget::gotModified); } bool FilesWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; for (const QString &keyStem : keyStart) for (int i = 1; i < 32; ++i) { /// FIXME replace number by constant const QString key = i > 1 ? keyStem + QString::number(i) : keyStem; entry->remove(key); } Value combinedValue; fileList->apply(combinedValue); Value urlValue, doiValue, localFileValue; urlValue.reserve(combinedValue.size()); doiValue.reserve(combinedValue.size()); localFileValue.reserve(combinedValue.size()); for (const auto &valueItem : const_cast(combinedValue)) { const QSharedPointer verbatimText = valueItem.dynamicCast(); if (!verbatimText.isNull()) { const QString text = verbatimText->text(); QRegularExpressionMatch match; if ((match = KBibTeX::urlRegExp.match(text)).hasMatch()) { /// add full URL VerbatimText *newVT = new VerbatimText(match.captured(0)); /// test for duplicates if (urlValue.contains(*newVT)) delete newVT; else urlValue.append(QSharedPointer(newVT)); } else if ((match = KBibTeX::doiRegExp.match(text)).hasMatch()) { /// add DOI VerbatimText *newVT = new VerbatimText(match.captured(0)); /// test for duplicates if (doiValue.contains(*newVT)) delete newVT; else doiValue.append(QSharedPointer(newVT)); } else { /// add anything else (e.g. local file) VerbatimText *newVT = new VerbatimText(*verbatimText); /// test for duplicates if (localFileValue.contains(*newVT)) delete newVT; else localFileValue.append(QSharedPointer(newVT)); } } } if (urlValue.isEmpty()) entry->remove(Entry::ftUrl); else entry->insert(Entry::ftUrl, urlValue); if (localFileValue.isEmpty()) { entry->remove(Entry::ftFile); entry->remove(Entry::ftLocalFile); } else if (Preferences::instance().bibliographySystem() == Preferences::BibLaTeX) { entry->remove(Entry::ftLocalFile); entry->insert(Entry::ftFile, localFileValue); } else if (Preferences::instance().bibliographySystem() == Preferences::BibTeX) { entry->remove(Entry::ftFile); entry->insert(Entry::ftLocalFile, localFileValue); } if (doiValue.isEmpty()) entry->remove(Entry::ftDOI); else entry->insert(Entry::ftDOI, doiValue); return true; } bool FilesWidget::reset(QSharedPointer element) { QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; Value combinedValue; for (const QString &keyStem : keyStart) for (int i = 1; i < 32; ++i) { /// FIXME replace number by constant const QString key = i > 1 ? keyStem + QString::number(i) : keyStem; const Value &value = entry->operator [](key); for (const auto &valueItem : const_cast(value)) combinedValue.append(valueItem); } fileList->setElement(element.data()); fileList->reset(combinedValue); return true; } bool FilesWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fileList->validate(widgetWithIssue, message); } void FilesWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); fileList->setReadOnly(isReadOnly); } QString FilesWidget::label() { return i18n("External"); } QIcon FilesWidget::icon() { return QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")); } void FilesWidget::setFile(const File *file) { ElementWidget::setFile(file); fileList->setFile(file); } bool FilesWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } const QStringList FilesWidget::keyStart {Entry::ftUrl, QStringLiteral("postscript"), Entry::ftLocalFile, Entry::ftDOI, Entry::ftFile, QStringLiteral("ee"), QStringLiteral("biburl")}; OtherFieldsWidget::OtherFieldsWidget(const QStringList &blacklistedFields, QWidget *parent) : ElementWidget(parent), blackListed(blacklistedFields) { internalEntry = QSharedPointer(new Entry()); createGUI(); } OtherFieldsWidget::~OtherFieldsWidget() { delete fieldContent; } bool OtherFieldsWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; for (const QString &key : const_cast(deletedKeys)) entry->remove(key); for (const QString &key : const_cast(modifiedKeys)) { entry->remove(key); entry->insert(key, internalEntry->value(key)); } return true; } bool OtherFieldsWidget::reset(QSharedPointer element) { QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; internalEntry = QSharedPointer(new Entry(*entry.data())); deletedKeys.clear(); // FIXME clearing list may be premature here... modifiedKeys.clear(); // FIXME clearing list may be premature here... updateList(); updateGUI(); return true; } bool OtherFieldsWidget::validate(QWidget **, QString &) const { /// No checks to make here; all actual check will be conducted in actionAddApply(..) return true; } void OtherFieldsWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); fieldName->setReadOnly(isReadOnly); fieldContent->setReadOnly(isReadOnly); /// will take care of enabled/disabling buttons updateGUI(); updateList(); } QString OtherFieldsWidget::label() { return i18n("Other Fields"); } QIcon OtherFieldsWidget::icon() { return QIcon::fromTheme(QStringLiteral("other")); } bool OtherFieldsWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } void OtherFieldsWidget::listElementExecuted(QTreeWidgetItem *item, int column) { Q_UNUSED(column) /// we do not care which column got clicked QString key = item->text(0); fieldName->setText(key); fieldContent->reset(internalEntry->value(key)); } void OtherFieldsWidget::listCurrentChanged(QTreeWidgetItem *item, QTreeWidgetItem *previous) { Q_UNUSED(previous) bool validUrl = false; bool somethingSelected = item != nullptr; buttonDelete->setEnabled(somethingSelected && !isReadOnly); if (somethingSelected) { currentUrl = QUrl(item->text(1)); validUrl = currentUrl.isValid() && currentUrl.isLocalFile() & QFileInfo::exists(currentUrl.toLocalFile()); if (!validUrl) { const QRegularExpressionMatch urlRegExpMatch = KBibTeX::urlRegExp.match(item->text(1)); if (urlRegExpMatch.hasMatch()) { currentUrl = QUrl(urlRegExpMatch.captured(0)); validUrl = currentUrl.isValid(); buttonOpen->setEnabled(validUrl); } } } if (!validUrl) currentUrl = QUrl(); buttonOpen->setEnabled(validUrl); } void OtherFieldsWidget::actionAddApply() { if (isReadOnly) return; /// never modify anything if in read-only mode QString key = fieldName->text(), message; Value value; if (!fieldContent->validate(nullptr, message)) return; ///< invalid values should not get applied if (!fieldContent->apply(value)) return; if (internalEntry->contains(key)) internalEntry->remove(key); internalEntry->insert(key, value); if (!modifiedKeys.contains(key)) modifiedKeys << key; updateList(); updateGUI(); gotModified(); } void OtherFieldsWidget::actionDelete() { if (isReadOnly) return; /// never modify anything if in read-only mode Q_ASSERT_X(otherFieldsList->currentItem() != nullptr, "OtherFieldsWidget::actionDelete", "otherFieldsList->currentItem() is NULL"); QString key = otherFieldsList->currentItem()->text(0); if (!deletedKeys.contains(key)) deletedKeys << key; internalEntry->remove(key); updateList(); updateGUI(); listCurrentChanged(otherFieldsList->currentItem(), nullptr); gotModified(); } void OtherFieldsWidget::actionOpen() { if (currentUrl.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(currentUrl); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type #if KIO_VERSION < 0x051f00 // < 5.31.0 KRun::runUrl(currentUrl, mimeTypeName, this, false, false); #else // KIO_VERSION < 0x051f00 // >= 5.31.0 KRun::runUrl(currentUrl, mimeTypeName, this, KRun::RunFlags()); #endif // KIO_VERSION < 0x051f00 } } void OtherFieldsWidget::createGUI() { QGridLayout *layout = new QGridLayout(this); /// set row and column stretches based on chosen layout layout->setColumnStretch(0, 0); layout->setColumnStretch(1, 1); layout->setColumnStretch(2, 0); layout->setRowStretch(0, 0); layout->setRowStretch(1, 1); layout->setRowStretch(2, 0); layout->setRowStretch(3, 0); layout->setRowStretch(4, 1); QLabel *label = new QLabel(i18n("Name:"), this); layout->addWidget(label, 0, 0, 1, 1); label->setAlignment((Qt::Alignment)label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)); fieldName = new KLineEdit(this); layout->addWidget(fieldName, 0, 1, 1, 1); label->setBuddy(fieldName); buttonAddApply = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), this); buttonAddApply->setEnabled(false); layout->addWidget(buttonAddApply, 0, 2, 1, 1); label = new QLabel(i18n("Content:"), this); layout->addWidget(label, 1, 0, 1, 1); label->setAlignment((Qt::Alignment)label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)); fieldContent = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfSource, KBibTeX::tfSource, this); layout->addWidget(fieldContent, 1, 1, 1, 2); label->setBuddy(fieldContent->buddy()); label = new QLabel(i18n("List:"), this); layout->addWidget(label, 2, 0, 1, 1); label->setAlignment((Qt::Alignment)label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)); otherFieldsList = new QTreeWidget(this); otherFieldsList->setHeaderLabels(QStringList {i18n("Key"), i18n("Value")}); otherFieldsList->setRootIsDecorated(false); layout->addWidget(otherFieldsList, 2, 1, 3, 1); label->setBuddy(otherFieldsList); buttonDelete = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Delete"), this); buttonDelete->setEnabled(false); layout->addWidget(buttonDelete, 2, 2, 1, 1); buttonOpen = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open"), this); buttonOpen->setEnabled(false); layout->addWidget(buttonOpen, 3, 2, 1, 1); connect(otherFieldsList, &QTreeWidget::itemActivated, this, &OtherFieldsWidget::listElementExecuted); connect(otherFieldsList, &QTreeWidget::currentItemChanged, this, &OtherFieldsWidget::listCurrentChanged); connect(otherFieldsList, &QTreeWidget::itemSelectionChanged, this, &OtherFieldsWidget::updateGUI); connect(fieldName, &KLineEdit::textEdited, this, &OtherFieldsWidget::updateGUI); connect(buttonAddApply, &QPushButton::clicked, this, &OtherFieldsWidget::actionAddApply); connect(buttonDelete, &QPushButton::clicked, this, &OtherFieldsWidget::actionDelete); connect(buttonOpen, &QPushButton::clicked, this, &OtherFieldsWidget::actionOpen); } void OtherFieldsWidget::updateList() { const QString selText = otherFieldsList->selectedItems().isEmpty() ? QString() : otherFieldsList->selectedItems().first()->text(0); const QString curText = otherFieldsList->currentItem() == nullptr ? QString() : otherFieldsList->currentItem()->text(0); otherFieldsList->clear(); for (Entry::ConstIterator it = internalEntry->constBegin(); it != internalEntry->constEnd(); ++it) if (!blackListed.contains(it.key().toLower())) { QTreeWidgetItem *item = new QTreeWidgetItem(); item->setText(0, it.key()); item->setText(1, PlainTextValue::text(it.value())); item->setIcon(0, QIcon::fromTheme(QStringLiteral("entry"))); // FIXME otherFieldsList->addTopLevelItem(item); item->setSelected(selText == it.key()); if (it.key() == curText) otherFieldsList->setCurrentItem(item); } } void OtherFieldsWidget::updateGUI() { QString key = fieldName->text(); if (key.isEmpty() || blackListed.contains(key, Qt::CaseInsensitive)) // TODO check for more (e.g. spaces) buttonAddApply->setEnabled(false); else { buttonAddApply->setEnabled(!isReadOnly); buttonAddApply->setText(internalEntry->contains(key) ? i18n("Apply") : i18n("Add")); buttonAddApply->setIcon(internalEntry->contains(key) ? QIcon::fromTheme(QStringLiteral("document-edit")) : QIcon::fromTheme(QStringLiteral("list-add"))); } } MacroWidget::MacroWidget(QWidget *parent) : ElementWidget(parent) { createGUI(); } MacroWidget::~MacroWidget() { delete fieldInputValue; } bool MacroWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer macro = element.dynamicCast(); if (macro.isNull()) return false; Value value; bool result = fieldInputValue->apply(value); macro->setValue(value); return result; } bool MacroWidget::reset(QSharedPointer element) { QSharedPointer macro = element.dynamicCast(); if (macro.isNull()) return false; return fieldInputValue->reset(macro->value()); } bool MacroWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fieldInputValue->validate(widgetWithIssue, message); } void MacroWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); fieldInputValue->setReadOnly(isReadOnly); } QString MacroWidget::label() { return i18n("Macro"); } QIcon MacroWidget::icon() { return QIcon::fromTheme(QStringLiteral("macro")); } bool MacroWidget::canEdit(const Element *element) { return Macro::isMacro(*element); } void MacroWidget::createGUI() { QBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(i18n("Value:"), this); layout->addWidget(label, 0); label->setAlignment((Qt::Alignment)label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)); fieldInputValue = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfSource, this); layout->addWidget(fieldInputValue, 1); label->setBuddy(fieldInputValue->buddy()); connect(fieldInputValue, &FieldInput::modified, this, &MacroWidget::gotModified); } PreambleWidget::PreambleWidget(QWidget *parent) : ElementWidget(parent) { createGUI(); } bool PreambleWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer preamble = element.dynamicCast(); if (preamble.isNull()) return false; Value value; bool result = fieldInputValue->apply(value); preamble->setValue(value); return result; } bool PreambleWidget::reset(QSharedPointer element) { QSharedPointer preamble = element.dynamicCast(); if (preamble.isNull()) return false; return fieldInputValue->reset(preamble->value()); } bool PreambleWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fieldInputValue->validate(widgetWithIssue, message); } void PreambleWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); fieldInputValue->setReadOnly(isReadOnly); } QString PreambleWidget::label() { return i18n("Preamble"); } QIcon PreambleWidget::icon() { return QIcon::fromTheme(QStringLiteral("preamble")); } bool PreambleWidget::canEdit(const Element *element) { return Preamble::isPreamble(*element); } void PreambleWidget::createGUI() { QBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(i18n("Value:"), this); layout->addWidget(label, 0); label->setAlignment((Qt::Alignment)label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)); fieldInputValue = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfSource, KBibTeX::tfSource, this); // FIXME: other editing modes beyond Source applicable? layout->addWidget(fieldInputValue, 1); label->setBuddy(fieldInputValue->buddy()); connect(fieldInputValue, &FieldInput::modified, this, &PreambleWidget::gotModified); } class SourceWidget::Private { public: KComboBox *messages; QPushButton *buttonRestore; FileImporterBibTeX *importerBibTeX; DelayedExecutionTimer *delayedExecutionTimer; Private(SourceWidget *parent) : messages(nullptr), buttonRestore(nullptr), importerBibTeX(new FileImporterBibTeX(parent)), delayedExecutionTimer(new DelayedExecutionTimer(1500, 500, parent)) { /// nothing } void addMessage(const FileImporter::MessageSeverity severity, const QString &messageText) { const QIcon icon = severity == FileImporter::SeverityInfo ? QIcon::fromTheme(QStringLiteral("dialog-information")) : (severity == FileImporter::SeverityWarning ? QIcon::fromTheme(QStringLiteral("dialog-warning")) : (severity == FileImporter::SeverityError ? QIcon::fromTheme(QStringLiteral("dialog-error")) : QIcon::fromTheme(QStringLiteral("dialog-question")))); messages->addItem(icon, messageText); } }; SourceWidget::SourceWidget(QWidget *parent) : ElementWidget(parent), elementClass(elementInvalid), d(new SourceWidget::Private(this)) { createGUI(); connect(document, &KTextEditor::Document::textChanged, d->delayedExecutionTimer, &DelayedExecutionTimer::trigger); connect(document, &KTextEditor::Document::textChanged, d->messages, &KComboBox::clear); connect(d->delayedExecutionTimer, &DelayedExecutionTimer::triggered, this, &SourceWidget::updateMessage); } SourceWidget::~SourceWidget() { delete document; delete d; } void SourceWidget::setElementClass(ElementClass elementClass) { this->elementClass = elementClass; updateMessage(); } bool SourceWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; ///< never save data if in read-only mode const QString text = document->text(); const QScopedPointer file(d->importerBibTeX->fromString(text)); if (file.isNull() || file->count() != 1) return false; QSharedPointer entry = element.dynamicCast(); QSharedPointer readEntry = file->first().dynamicCast(); if (!readEntry.isNull() && !entry.isNull()) { if (elementClass != elementEntry) return false; ///< Source widget should only edit Entry objects entry->operator =(*readEntry.data()); //entry = readEntry; return true; } else { QSharedPointer macro = element.dynamicCast(); QSharedPointer readMacro = file->first().dynamicCast(); if (!readMacro.isNull() && !macro.isNull()) { if (elementClass != elementMacro) return false; ///< Source widget should only edit Macro objects macro->operator =(*readMacro.data()); return true; } else { QSharedPointer preamble = element.dynamicCast(); QSharedPointer readPreamble = file->first().dynamicCast(); if (!readPreamble.isNull() && !preamble.isNull()) { if (elementClass != elementPreamble) return false; ///< Source widget should only edit Preamble objects preamble->operator =(*readPreamble.data()); return true; } else { qCWarning(LOG_KBIBTEX_GUI) << "Do not know how to apply source code"; return false; } } } } bool SourceWidget::reset(QSharedPointer element) { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widget's value disconnect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); FileExporterBibTeX exporter(this); exporter.setEncoding(QStringLiteral("utf-8")); const QString exportedText = exporter.toString(element, m_file); if (!exportedText.isEmpty()) { originalText = exportedText; /// Limitation of KTextEditor: If editor is read-only, no text can be set /// Therefore, temporarily lift read-only status const bool originalReadWriteStatus = document->isReadWrite(); document->setReadWrite(true); const bool settingTextSuccessful = document->setText(originalText); if (!settingTextSuccessful) qCWarning(LOG_KBIBTEX_GUI) << "Could not set BibTeX source code to source editor"; document->setReadWrite(originalReadWriteStatus); } else qCWarning(LOG_KBIBTEX_GUI) << "Converting entry to BibTeX source resulting in empty text"; connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); return !exportedText.isEmpty(); } bool SourceWidget::validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); d->messages->clear(); const QString text = document->text(); connect(d->importerBibTeX, &FileImporterBibTeX::message, this, &SourceWidget::addMessage); const QScopedPointer file(d->importerBibTeX->fromString(text)); disconnect(d->importerBibTeX, &FileImporterBibTeX::message, this, &SourceWidget::addMessage); if (file.isNull() || file->count() != 1) { - if (widgetWithIssue != nullptr) *widgetWithIssue = document->views().first(); ///< We create one view initially, so this should never fail + if (widgetWithIssue != nullptr) *widgetWithIssue = document->views().at(0); ///< We create one view initially, so this should never fail message = i18n("Given source code does not parse as one single BibTeX element."); return false; } bool result = false; switch (elementClass) { case elementEntry: { QSharedPointer entry = file->first().dynamicCast(); result = !entry.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX entry."); } break; case elementMacro: { QSharedPointer macro = file->first().dynamicCast(); result = !macro.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX macro."); } break; case elementPreamble: { QSharedPointer preamble = file->first().dynamicCast(); result = !preamble.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX preamble."); } break; // case elementComment // TODO? default: message = QString(QStringLiteral("elementClass is unknown: %1")).arg(elementClass); result = false; } if (!result && widgetWithIssue != nullptr) - *widgetWithIssue = document->views().first(); ///< We create one view initially, so this should never fail + *widgetWithIssue = document->views().at(0); ///< We create one view initially, so this should never fail if (message.isEmpty() && d->messages->count() == 0) d->addMessage(FileImporter::SeverityInfo, i18n("No issues detected")); return result; } void SourceWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); d->buttonRestore->setEnabled(!isReadOnly); document->setReadWrite(!isReadOnly); } QString SourceWidget::label() { return i18n("Source"); } QIcon SourceWidget::icon() { return QIcon::fromTheme(QStringLiteral("code-context")); } bool SourceWidget::canEdit(const Element *element) { Q_UNUSED(element) return true; /// source widget should be able to edit any element } void SourceWidget::createGUI() { QGridLayout *layout = new QGridLayout(this); layout->setColumnStretch(0, 1); layout->setColumnStretch(1, 0); layout->setRowStretch(0, 1); layout->setRowStretch(1, 0); KTextEditor::Editor *editor = KTextEditor::Editor::instance(); document = editor->createDocument(this); document->setHighlightingMode(QStringLiteral("BibTeX")); KTextEditor::View *view = document->createView(this); layout->addWidget(view, 0, 0, 1, 2); d->messages = new KComboBox(this); layout->addWidget(d->messages, 1, 0, 1, 1); d->buttonRestore = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Restore"), this); layout->addWidget(d->buttonRestore, 1, 1, 1, 1); connect(d->buttonRestore, &QPushButton::clicked, this, static_cast(&SourceWidget::reset)); connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); } void SourceWidget::reset() { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widget's value disconnect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); document->setText(originalText); setModified(false); connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); } void SourceWidget::addMessage(const FileImporter::MessageSeverity severity, const QString &messageText) { d->addMessage(severity, messageText); } void SourceWidget::updateMessage() { QString message; const bool validationResult = validate(nullptr, message); if (!message.isEmpty()) { if (validationResult) addMessage(FileImporter::SeverityInfo, message); else addMessage(FileImporter::SeverityError, message); } } #include "elementwidgets.moc" diff --git a/src/io/fileimporterbibtex.cpp b/src/io/fileimporterbibtex.cpp index ff51ea43..f09fb166 100644 --- a/src/io/fileimporterbibtex.cpp +++ b/src/io/fileimporterbibtex.cpp @@ -1,1279 +1,1279 @@ /*************************************************************************** * Copyright (C) 2004-2018 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "fileimporterbibtex.h" #include #include #include #include #include #include "preferences.h" #include "file.h" #include "comment.h" #include "macro.h" #include "preamble.h" #include "entry.h" #include "element.h" #include "value.h" #include "encoderlatex.h" #include "bibtexentries.h" #include "bibtexfields.h" #include "logging_io.h" const char *FileImporterBibTeX::defaultCodecName = "utf-8"; FileImporterBibTeX::FileImporterBibTeX(QObject *parent) : FileImporter(parent), m_cancelFlag(false), m_textStream(nullptr), m_commentHandling(IgnoreComments), m_keywordCasing(KBibTeX::cLowerCase), m_lineNo(1) { m_keysForPersonDetection.append(Entry::ftAuthor); m_keysForPersonDetection.append(Entry::ftEditor); m_keysForPersonDetection.append(QStringLiteral("bookauthor")); /// used by JSTOR } File *FileImporterBibTeX::load(QIODevice *iodevice) { m_cancelFlag = false; if (!iodevice->isReadable() && !iodevice->open(QIODevice::ReadOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Input device not readable"; emit message(SeverityError, QStringLiteral("Input device not readable")); return nullptr; } File *result = new File(); /// Used to determine if file prefers quotation marks over /// curly brackets or the other way around m_statistics.countCurlyBrackets = 0; m_statistics.countQuotationMarks = 0; m_statistics.countFirstNameFirst = 0; m_statistics.countLastNameFirst = 0; m_statistics.countNoCommentQuote = 0; m_statistics.countCommentPercent = 0; m_statistics.countCommentCommand = 0; m_statistics.countProtectedTitle = 0; m_statistics.countUnprotectedTitle = 0; m_statistics.mostRecentListSeparator.clear(); m_textStream = new QTextStream(iodevice); m_textStream->setCodec(defaultCodecName); ///< unless we learn something else, assume default codec result->setProperty(File::Encoding, QStringLiteral("latex")); QString rawText; while (!m_textStream->atEnd()) { QString line = m_textStream->readLine(); bool skipline = evaluateParameterComments(m_textStream, line.toLower(), result); // FIXME XML data should be removed somewhere else? onlinesearch ... if (line.startsWith(QStringLiteral(""))) /// Hop over XML declarations skipline = true; if (!skipline) rawText.append(line).append("\n"); } delete m_textStream; /** Remove HTML code from the input source */ // FIXME HTML data should be removed somewhere else? onlinesearch ... const int originalLength = rawText.length(); rawText = rawText.remove(KBibTeX::htmlRegExp); const int afterHTMLremovalLength = rawText.length(); if (originalLength != afterHTMLremovalLength) { qCInfo(LOG_KBIBTEX_IO) << (originalLength - afterHTMLremovalLength) << "characters of HTML tags have been removed"; emit message(SeverityInfo, QString(QStringLiteral("%1 characters of HTML tags have been removed")).arg(originalLength - afterHTMLremovalLength)); } // TODO really necessary to pipe data through several QTextStreams? m_textStream = new QTextStream(&rawText, QIODevice::ReadOnly); m_textStream->setCodec(defaultCodecName); m_lineNo = 1; m_prevLine = m_currentLine = QString(); m_knownElementIds.clear(); readChar(); while (!m_nextChar.isNull() && !m_cancelFlag && !m_textStream->atEnd()) { emit progress(m_textStream->pos(), rawText.length()); Element *element = nextElement(); if (element != nullptr) { if (m_commentHandling == KeepComments || !Comment::isComment(*element)) result->append(QSharedPointer(element)); else delete element; } } emit progress(100, 100); if (m_cancelFlag) { qCWarning(LOG_KBIBTEX_IO) << "Loading bibliography data has been canceled"; emit message(SeverityError, QStringLiteral("Loading bibliography data has been canceled")); delete result; result = nullptr; } delete m_textStream; if (result != nullptr) { /// Set the file's preferences for string delimiters /// deduced from statistics built while parsing the file result->setProperty(File::StringDelimiter, m_statistics.countQuotationMarks > m_statistics.countCurlyBrackets ? QStringLiteral("\"\"") : QStringLiteral("{}")); /// Set the file's preferences for name formatting result->setProperty(File::NameFormatting, m_statistics.countFirstNameFirst > m_statistics.countLastNameFirst ? Preferences::personNameFormatFirstLast : Preferences::personNameFormatLastFirst); /// Set the file's preferences for title protected Qt::CheckState triState = (m_statistics.countProtectedTitle > m_statistics.countUnprotectedTitle * 4) ? Qt::Checked : ((m_statistics.countProtectedTitle * 4 < m_statistics.countUnprotectedTitle) ? Qt::Unchecked : Qt::PartiallyChecked); result->setProperty(File::ProtectCasing, static_cast(triState)); /// Set the file's preferences for quoting of comments if (m_statistics.countNoCommentQuote > m_statistics.countCommentCommand && m_statistics.countNoCommentQuote > m_statistics.countCommentPercent) result->setProperty(File::QuoteComment, static_cast(Preferences::qcNone)); else if (m_statistics.countCommentCommand > m_statistics.countNoCommentQuote && m_statistics.countCommentCommand > m_statistics.countCommentPercent) result->setProperty(File::QuoteComment, static_cast(Preferences::qcCommand)); else result->setProperty(File::QuoteComment, static_cast(Preferences::qcPercentSign)); if (!m_statistics.mostRecentListSeparator.isEmpty()) result->setProperty(File::ListSeparator, m_statistics.mostRecentListSeparator); // TODO gather more statistics for keyword casing etc. } iodevice->close(); return result; } bool FileImporterBibTeX::guessCanDecode(const QString &rawText) { static const QRegularExpression bibtexLikeText(QStringLiteral("@\\w+\\{.+\\}")); QString text = EncoderLaTeX::instance().decode(rawText); return bibtexLikeText.match(text).hasMatch(); } void FileImporterBibTeX::cancel() { m_cancelFlag = true; } Element *FileImporterBibTeX::nextElement() { Token token = nextToken(); if (token == tAt) { QString elementType = readSimpleString(); if (elementType.toLower() == QStringLiteral("comment")) { ++m_statistics.countCommentCommand; return readCommentElement(); } else if (elementType.toLower() == QStringLiteral("string")) return readMacroElement(); else if (elementType.toLower() == QStringLiteral("preamble")) return readPreambleElement(); else if (elementType.toLower() == QStringLiteral("import")) { qCDebug(LOG_KBIBTEX_IO) << "Skipping potential HTML/JavaScript @import statement near line" << m_lineNo; emit message(SeverityInfo, QString(QStringLiteral("Skipping potential HTML/JavaScript @import statement near line %1")).arg(m_lineNo)); return nullptr; } else if (!elementType.isEmpty()) return readEntryElement(elementType); else { qCWarning(LOG_KBIBTEX_IO) << "Element type after '@' is empty or invalid near line" << m_lineNo; emit message(SeverityError, QString(QStringLiteral("Element type after '@' is empty or invalid near line %1")).arg(m_lineNo)); return nullptr; } } else if (token == tUnknown && m_nextChar == QLatin1Char('%')) { /// do not complain about LaTeX-like comments, just eat them ++m_statistics.countCommentPercent; return readPlainCommentElement(QString()); } else if (token == tUnknown) { if (m_nextChar.isLetter()) { qCDebug(LOG_KBIBTEX_IO) << "Unknown character" << m_nextChar << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << ", treating as comment"; emit message(SeverityInfo, QString(QStringLiteral("Unknown character '%1' near line %2, treating as comment")).arg(m_nextChar).arg(m_lineNo)); } else if (m_nextChar.isPrint()) { qCDebug(LOG_KBIBTEX_IO) << "Unknown character" << m_nextChar << "(" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << ") near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << ", treating as comment"; emit message(SeverityInfo, QString(QStringLiteral("Unknown character '%1' (0x%2) near line %3, treating as comment")).arg(m_nextChar).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(m_lineNo)); } else { qCDebug(LOG_KBIBTEX_IO) << "Unknown character" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << ", treating as comment"; emit message(SeverityInfo, QString(QStringLiteral("Unknown character 0x%1 near line %2, treating as comment")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(m_lineNo)); } ++m_statistics.countNoCommentQuote; return readPlainCommentElement(QString(m_prevChar) + m_nextChar); } if (token != tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Don't know how to parse next token of type" << tokenidToString(token) << "in line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << endl; emit message(SeverityError, QString(QStringLiteral("Don't know how to parse next token of type %1 in line %2")).arg(tokenidToString(token)).arg(m_lineNo)); } return nullptr; } Comment *FileImporterBibTeX::readCommentElement() { if (!readCharUntil(QStringLiteral("{("))) return nullptr; return new Comment(EncoderLaTeX::instance().decode(readBracketString())); } Comment *FileImporterBibTeX::readPlainCommentElement(const QString &prefix) { QString result = EncoderLaTeX::instance().decode(prefix + readLine()); while (m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r')) readChar(); while (!m_nextChar.isNull() && m_nextChar != QLatin1Char('@')) { const QChar nextChar = m_nextChar; const QString line = readLine(); while (m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r')) readChar(); result.append(EncoderLaTeX::instance().decode((nextChar == QLatin1Char('%') ? QString() : QString(nextChar)) + line)); } if (result.startsWith(QStringLiteral("x-kbibtex"))) { qCWarning(LOG_KBIBTEX_IO) << "Plain comment element starts with 'x-kbibtex', this should not happen"; emit message(SeverityWarning, QStringLiteral("Plain comment element starts with 'x-kbibtex', this should not happen")); /// ignore special comments return nullptr; } return new Comment(result); } Macro *FileImporterBibTeX::readMacroElement() { Token token = nextToken(); while (token != tBracketOpen) { if (token == tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing macro near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Opening curly brace '{' expected"; emit message(SeverityError, QString(QStringLiteral("Error in parsing macro near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); return nullptr; } token = nextToken(); } QString key = readSimpleString(); if (key.isEmpty()) { /// Cope with empty keys, /// duplicates are handled further below key = QStringLiteral("EmptyId"); } else if (!EncoderLaTeX::containsOnlyAscii(key)) { /// Try to avoid non-ascii characters in ids const QString newKey = EncoderLaTeX::instance().convertToPlainAscii(key); qCWarning(LOG_KBIBTEX_IO) << "Macro key" << key << "near line" << m_lineNo << "contains non-ASCII characters, converted to" << newKey; emit message(SeverityWarning, QString(QStringLiteral("Macro key '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(key).arg(m_lineNo).arg(newKey)); key = newKey; } /// Check for duplicate entry ids, avoid collisions if (m_knownElementIds.contains(key)) { static const QString newIdPattern = QStringLiteral("%1-%2"); int idx = 2; QString newKey = newIdPattern.arg(key).arg(idx); while (m_knownElementIds.contains(newKey)) newKey = newIdPattern.arg(key).arg(++idx); qCDebug(LOG_KBIBTEX_IO) << "Duplicate macro key" << key << ", using replacement key" << newKey; - emit message(SeverityWarning, QString(QStringLiteral("Duplicate macro key '%1', using replacement key '%2'")).arg(key).arg(newKey)); + emit message(SeverityWarning, QString(QStringLiteral("Duplicate macro key '%1', using replacement key '%2'")).arg(key, newKey)); key = newKey; } m_knownElementIds.insert(key); if (nextToken() != tAssign) { qCCritical(LOG_KBIBTEX_IO) << "Error in parsing macro" << key << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Assign symbol '=' expected"; emit message(SeverityError, QString(QStringLiteral("Error in parsing macro '%1' near line %2: Assign symbol '=' expected")).arg(key).arg(m_lineNo)); return nullptr; } Macro *macro = new Macro(key); do { bool isStringKey = false; QString text = readString(isStringKey); if (text.isNull()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing macro" << key << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Could not read macro's text"; emit message(SeverityError, QString(QStringLiteral("Error in parsing macro '%1' near line %2: Could not read macro's text")).arg(key).arg(m_lineNo)); delete macro; } text = EncoderLaTeX::instance().decode(bibtexAwareSimplify(text)); if (isStringKey) macro->value().append(QSharedPointer(new MacroKey(text))); else macro->value().append(QSharedPointer(new PlainText(text))); token = nextToken(); } while (token == tDoublecross); return macro; } Preamble *FileImporterBibTeX::readPreambleElement() { Token token = nextToken(); while (token != tBracketOpen) { if (token == tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing preamble near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Opening curly brace '{' expected"; emit message(SeverityError, QString(QStringLiteral("Error in parsing preamble near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); return nullptr; } token = nextToken(); } Preamble *preamble = new Preamble(); do { bool isStringKey = false; QString text = readString(isStringKey); if (text.isNull()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing preamble near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Could not read preamble's text"; emit message(SeverityError, QString(QStringLiteral("Error in parsing preamble near line %1: Could not read preamble's text")).arg(m_lineNo)); delete preamble; return nullptr; } /// Remember: strings from preamble do not get encoded, /// may contain raw LaTeX commands and code text = bibtexAwareSimplify(text); if (isStringKey) preamble->value().append(QSharedPointer<MacroKey>(new MacroKey(text))); else preamble->value().append(QSharedPointer<PlainText>(new PlainText(text))); token = nextToken(); } while (token == tDoublecross); return preamble; } Entry *FileImporterBibTeX::readEntryElement(const QString &typeString) { Token token = nextToken(); while (token != tBracketOpen) { if (token == tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Opening curly brace '{' expected"; emit message(SeverityError, QString(QStringLiteral("Error in parsing entry near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); return nullptr; } token = nextToken(); } QString id = readSimpleString(QStringLiteral(",}")).trimmed(); if (id.isEmpty()) { if (m_nextChar == QLatin1Char(',') || m_nextChar == QLatin1Char('}')) { /// Cope with empty ids, /// duplicates are handled further below id = QStringLiteral("EmptyId"); } else { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry near line" << m_lineNo << ":" << m_prevLine << endl << m_currentLine << "): Could not read entry id"; emit message(SeverityError, QString(QStringLiteral("Error in parsing preambentryle near line %1: Could not read entry id")).arg(m_lineNo)); return nullptr; } } else if (!EncoderLaTeX::containsOnlyAscii(id)) { /// Try to avoid non-ascii characters in ids const QString newId = EncoderLaTeX::instance().convertToPlainAscii(id); qCWarning(LOG_KBIBTEX_IO) << "Entry id" << id << "near line" << m_lineNo << "contains non-ASCII characters, converted to" << newId; emit message(SeverityWarning, QString(QStringLiteral("Entry id '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(id).arg(m_lineNo).arg(newId)); id = newId; } static const QVector<QChar> invalidIdCharacters = {QLatin1Char('{'), QLatin1Char('}'), QLatin1Char(',')}; for (const QChar &invalidIdCharacter : invalidIdCharacters) if (id.contains(invalidIdCharacter)) { qCWarning(LOG_KBIBTEX_IO) << "Entry id" << id << "near line" << m_lineNo << "contains invalid character" << invalidIdCharacter; emit message(SeverityError, QString(QStringLiteral("Entry id '%1' near line %2 contains invalid character '%3'")).arg(id).arg(m_lineNo).arg(invalidIdCharacter)); return nullptr; } /// Check for duplicate entry ids, avoid collisions if (m_knownElementIds.contains(id)) { static const QString newIdPattern = QStringLiteral("%1-%2"); int idx = 2; QString newId = newIdPattern.arg(id).arg(idx); while (m_knownElementIds.contains(newId)) newId = newIdPattern.arg(id).arg(++idx); qCDebug(LOG_KBIBTEX_IO) << "Duplicate id" << id << "near line" << m_lineNo << ", using replacement id" << newId; emit message(SeverityInfo, QString(QStringLiteral("Duplicate id '%1' near line %2, using replacement id '%3'")).arg(id).arg(m_lineNo).arg(newId)); id = newId; } m_knownElementIds.insert(id); Entry *entry = new Entry(BibTeXEntries::instance().format(typeString, m_keywordCasing), id); token = nextToken(); do { if (token == tBracketClose) break; else if (token == tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Unexpected end of data in entry" << id << "near line" << m_lineNo << ":" << m_prevLine << endl << m_currentLine; emit message(SeverityError, QString(QStringLiteral("Unexpected end of data in entry '%1' near line %2")).arg(id).arg(m_lineNo)); delete entry; return nullptr; } else if (token != tComma) { if (m_nextChar.isLetter()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Comma symbol ',' expected but got character" << m_nextChar << "(token" << tokenidToString(token) << ")"; emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character '%3' (token %4)")).arg(id).arg(m_lineNo).arg(m_nextChar).arg(tokenidToString(token))); } else if (m_nextChar.isPrint()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Comma symbol ',' expected but got character" << m_nextChar << "(" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << ", token" << tokenidToString(token) << ")"; emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character '%3' (0x%4, token %5)")).arg(id).arg(m_lineNo).arg(m_nextChar).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(tokenidToString(token))); } else { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Comma symbol (,) expected but got character" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << "(token" << tokenidToString(token) << ")"; emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character 0x%3 (token %4)")).arg(id).arg(m_lineNo).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(tokenidToString(token))); } delete entry; return nullptr; } QString keyName = BibTeXFields::instance().format(readSimpleString(), m_keywordCasing); if (keyName.isEmpty()) { token = nextToken(); if (token == tBracketClose) { /// Most often it is the case that the previous line ended with a comma, /// implying that this entry continues, but instead it gets closed by /// a closing curly bracket. qCDebug(LOG_KBIBTEX_IO) << "Issue while parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Last key-value pair ended with a non-conformant comma, ignoring that"; emit message(SeverityInfo, QString(QStringLiteral("Issue while parsing entry '%1' near line %2: Last key-value pair ended with a non-conformant comma, ignoring that")).arg(id).arg(m_lineNo)); break; } else { /// Something looks terribly wrong qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Closing curly bracket expected, but found" << tokenidToString(token); emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Closing curly bracket expected, but found %3")).arg(id).arg(m_lineNo).arg(tokenidToString(token))); delete entry; return nullptr; } } /// Try to avoid non-ascii characters in keys const QString newkeyName = EncoderLaTeX::instance().convertToPlainAscii(keyName); if (newkeyName != keyName) { qCWarning(LOG_KBIBTEX_IO) << "Field name " << keyName << "near line" << m_lineNo << "contains non-ASCII characters, converted to" << newkeyName; emit message(SeverityWarning, QString(QStringLiteral("Field name '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(keyName).arg(m_lineNo).arg(newkeyName)); keyName = newkeyName; } token = nextToken(); if (token != tAssign) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << ", field name" << keyName << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Assign symbol '=' expected after field name"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1', field name '%2' near line %3: Assign symbol '=' expected after field name")).arg(id).arg(keyName).arg(m_lineNo)); + emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1', field name '%2' near line %3: Assign symbol '=' expected after field name")).arg(id, keyName).arg(m_lineNo)); delete entry; return nullptr; } Value value; /// check for duplicate fields if (entry->contains(keyName)) { if (keyName.toLower() == Entry::ftKeywords || keyName.toLower() == Entry::ftUrl) { /// Special handling of keywords and URLs: instead of using fallback names /// like "keywords2", "keywords3", ..., append new keywords to /// already existing keyword value value = entry->value(keyName); } else if (m_keysForPersonDetection.contains(keyName.toLower())) { /// Special handling of authors and editors: instead of using fallback names /// like "author2", "author3", ..., append new authors to /// already existing author value value = entry->value(keyName); } else { int i = 2; QString appendix = QString::number(i); while (entry->contains(keyName + appendix)) { ++i; appendix = QString::number(i); } qCDebug(LOG_KBIBTEX_IO) << "Entry" << id << "already contains a key" << keyName << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "), using" << (keyName + appendix); - emit message(SeverityWarning, QString(QStringLiteral("Entry '%1' already contains a key '%2' near line %3, using '%4'")).arg(id).arg(keyName).arg(m_lineNo).arg(keyName + appendix)); + emit message(SeverityWarning, QString(QStringLiteral("Entry '%1' already contains a key '%2' near line %4, using '%3'")).arg(id, keyName, keyName + appendix).arg(m_lineNo)); keyName += appendix; } } token = readValue(value, keyName); if (token != tBracketClose && token != tComma) { qCWarning(LOG_KBIBTEX_IO) << "Failed to read value in entry" << id << ", field name" << keyName << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")"; - emit message(SeverityError, QString(QStringLiteral("Failed to read value in entry '%1', field name '%2' near line %3")).arg(id).arg(keyName).arg(m_lineNo)); + emit message(SeverityError, QString(QStringLiteral("Failed to read value in entry '%1', field name '%2' near line %3")).arg(id, keyName).arg(m_lineNo)); delete entry; return nullptr; } entry->insert(keyName, value); } while (true); return entry; } FileImporterBibTeX::Token FileImporterBibTeX::nextToken() { if (!skipWhiteChar()) { /// Some error occurred while reading from data stream return tEOF; } Token result = tUnknown; switch (m_nextChar.toLatin1()) { case '@': result = tAt; break; case '{': case '(': result = tBracketOpen; break; case '}': case ')': result = tBracketClose; break; case ',': result = tComma; break; case '=': result = tAssign; break; case '#': result = tDoublecross; break; default: if (m_textStream->atEnd()) result = tEOF; } if (m_nextChar != QLatin1Char('%')) { /// Unclean solution, but necessary for comments /// that have a percent sign as a prefix readChar(); } return result; } QString FileImporterBibTeX::readString(bool &isStringKey) { /// Most often it is not a string key isStringKey = false; if (!skipWhiteChar()) { /// Some error occurred while reading from data stream return QString::null; } switch (m_nextChar.toLatin1()) { case '{': case '(': { ++m_statistics.countCurlyBrackets; const QString result = readBracketString(); return result; } case '"': { ++m_statistics.countQuotationMarks; const QString result = readQuotedString(); return result; } default: isStringKey = true; const QString result = readSimpleString(); return result; } } QString FileImporterBibTeX::readSimpleString(const QString &until) { static const QString extraAlphaNumChars = QString(QStringLiteral("?'`-_:.+/$\\\"&")); QString result; ///< 'result' is Null on purpose: simple strings cannot be empty in contrast to e.g. quoted strings if (!skipWhiteChar()) { /// Some error occurred while reading from data stream return QString::null; } while (!m_nextChar.isNull()) { const ushort nextCharUnicode = m_nextChar.unicode(); if (!until.isEmpty()) { /// Variable "until" has user-defined value if (m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r') || until.contains(m_nextChar)) { /// Force break on line-breaks or if one of the "until" chars has been read break; } else { /// Append read character to final result result.append(m_nextChar); } } else if ((nextCharUnicode >= (ushort)'a' && nextCharUnicode <= (ushort)'z') || (nextCharUnicode >= (ushort)'A' && nextCharUnicode <= (ushort)'Z') || (nextCharUnicode >= (ushort)'0' && nextCharUnicode <= (ushort)'9') || extraAlphaNumChars.contains(m_nextChar)) { /// Accept default set of alpha-numeric characters result.append(m_nextChar); } else break; if (!readChar()) break; } return result; } QString FileImporterBibTeX::readQuotedString() { - QString result(QStringLiteral("")); + QString result; Q_ASSERT_X(m_nextChar == QLatin1Char('"'), "QString FileImporterBibTeX::readQuotedString()", "m_nextChar is not '\"'"); if (!readChar()) return QString::null; while (!m_nextChar.isNull()) { if (m_nextChar == QLatin1Char('"') && m_prevChar != QLatin1Char('\\') && m_prevChar != QLatin1Char('{')) break; else result.append(m_nextChar); if (!readChar()) return QString::null; } if (!readChar()) return QString::null; /// Remove protection around quotation marks result.replace(QStringLiteral("{\"}"), QStringLiteral("\"")); return result; } QString FileImporterBibTeX::readBracketString() { static const QChar backslash = QLatin1Char('\\'); - QString result(QStringLiteral("")); + QString result; const QChar openingBracket = m_nextChar; const QChar closingBracket = openingBracket == QLatin1Char('{') ? QLatin1Char('}') : (openingBracket == QLatin1Char('(') ? QLatin1Char(')') : QChar()); Q_ASSERT_X(!closingBracket.isNull(), "QString FileImporterBibTeX::readBracketString()", "openingBracket==m_nextChar is neither '{' nor '('"); int counter = 1; if (!readChar()) return QString::null; while (!m_nextChar.isNull()) { if (m_nextChar == openingBracket && m_prevChar != backslash) ++counter; else if (m_nextChar == closingBracket && m_prevChar != backslash) --counter; if (counter == 0) { break; } else result.append(m_nextChar); if (!readChar()) return QString::null; } if (!readChar()) return QString::null; return result; } FileImporterBibTeX::Token FileImporterBibTeX::readValue(Value &value, const QString &key) { Token token = tUnknown; const QString iKey = key.toLower(); static const QSet<QString> verbatimKeys {Entry::ftColor.toLower(), Entry::ftCrossRef.toLower(), Entry::ftXData.toLower()}; do { bool isStringKey = false; const QString rawText = readString(isStringKey); if (rawText.isNull()) return tEOF; QString text = EncoderLaTeX::instance().decode(rawText); /// for all entries except for abstracts ... if (iKey != Entry::ftAbstract && !(iKey.startsWith(Entry::ftUrl) && !iKey.startsWith(Entry::ftUrlDate)) && !iKey.startsWith(Entry::ftLocalFile) && !iKey.startsWith(Entry::ftFile)) { /// ... remove redundant spaces including newlines text = bibtexAwareSimplify(text); } /// abstracts will keep their formatting (regarding line breaks) /// as requested by Thomas Jensch via mail (20 October 2010) /// Maintain statistics on if (book) titles are protected /// by surrounding curly brackets if (iKey == Entry::ftTitle || iKey == Entry::ftBookTitle) { if (text[0] == QLatin1Char('{') && text[text.length() - 1] == QLatin1Char('}')) ++m_statistics.countProtectedTitle; else ++m_statistics.countUnprotectedTitle; } if (m_keysForPersonDetection.contains(iKey)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { CommaContainment comma = ccContainsComma; parsePersonList(text, value, &comma, m_lineNo, this); /// Update statistics on name formatting if (comma == ccContainsComma) ++m_statistics.countLastNameFirst; else ++m_statistics.countFirstNameFirst; } } else if (iKey == Entry::ftPages) { static const QRegularExpression rangeInAscii(QStringLiteral("\\s*--?\\s*")); text.replace(rangeInAscii, QChar(0x2013)); if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else value.append(QSharedPointer<PlainText>(new PlainText(text))); } else if ((iKey.startsWith(Entry::ftUrl) && !iKey.startsWith(Entry::ftUrlDate)) || iKey.startsWith(Entry::ftLocalFile) || iKey.startsWith(Entry::ftFile) || iKey == QStringLiteral("ee") || iKey == QStringLiteral("biburl")) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { /// Assumption: in fields like Url or LocalFile, file names are separated by ; static const QRegularExpression semicolonSpace = QRegularExpression(QStringLiteral("[;]\\s*")); const QStringList fileList = rawText.split(semicolonSpace, QString::SkipEmptyParts); for (const QString &filename : fileList) { value.append(QSharedPointer<VerbatimText>(new VerbatimText(filename))); } } } else if (iKey.startsWith(Entry::ftFile)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { /// Assumption: this field was written by Mendeley, which uses /// a very strange format for file names: /// :C$\backslash$:/Users/BarisEvrim/Documents/Mendeley Desktop/GeversPAMI10.pdf:pdf /// :: /// :Users/Fred/Library/Application Support/Mendeley Desktop/Downloaded/Hasselman et al. - 2011 - (Still) Growing Up What should we be a realist about in the cognitive and behavioural sciences Abstract.pdf:pdf const QRegularExpressionMatch match = KBibTeX::mendeleyFileRegExp.match(rawText); if (match.hasMatch()) { static const QString backslashLaTeX = QStringLiteral("$\\backslash$"); QString filename = match.captured(1).remove(backslashLaTeX); if (filename.startsWith(QStringLiteral("home/")) || filename.startsWith(QStringLiteral("Users/"))) { /// Mendeley doesn't have a slash at the beginning of absolute paths, /// so, insert one /// See bug 19833, comment 5: https://gna.org/bugs/index.php?19833#comment5 filename.prepend(QLatin1Char('/')); } value.append(QSharedPointer<VerbatimText>(new VerbatimText(filename))); } else value.append(QSharedPointer<VerbatimText>(new VerbatimText(text))); } } else if (iKey == Entry::ftMonth) { if (isStringKey) { static const QRegularExpression monthThreeChars(QStringLiteral("^[a-z]{3}"), QRegularExpression::CaseInsensitiveOption); if (monthThreeChars.match(text).hasMatch()) text = text.left(3).toLower(); value.append(QSharedPointer<MacroKey>(new MacroKey(text))); } else value.append(QSharedPointer<PlainText>(new PlainText(text))); } else if (iKey.startsWith(Entry::ftDOI)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { /// Take care of "; " which separates multiple DOIs, but which may baffle the regexp QString preprocessedText = rawText; preprocessedText.replace(QStringLiteral("; "), QStringLiteral(" ")); /// Extract everything that looks like a DOI using a regular expression, /// ignore everything else QRegularExpressionMatchIterator doiRegExpMatchIt = KBibTeX::doiRegExp.globalMatch(preprocessedText); while (doiRegExpMatchIt.hasNext()) { const QRegularExpressionMatch doiRegExpMatch = doiRegExpMatchIt.next(); value.append(QSharedPointer<VerbatimText>(new VerbatimText(doiRegExpMatch.captured(0)))); } } } else if (iKey == Entry::ftKeywords) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { char splitChar; const QList<QSharedPointer<Keyword> > keywords = splitKeywords(text, &splitChar); for (const auto &keyword : keywords) value.append(keyword); /// Memorize (some) split characters for later use /// (e.g. when writing file again) if (splitChar == ';') m_statistics.mostRecentListSeparator = QStringLiteral("; "); else if (splitChar == ',') m_statistics.mostRecentListSeparator = QStringLiteral(", "); } } else if (verbatimKeys.contains(iKey)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else value.append(QSharedPointer<VerbatimText>(new VerbatimText(rawText))); } else { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else value.append(QSharedPointer<PlainText>(new PlainText(text))); } token = nextToken(); } while (token == tDoublecross); return token; } bool FileImporterBibTeX::readChar() { /// Memorize previous char m_prevChar = m_nextChar; if (m_textStream->atEnd()) { /// At end of data stream m_nextChar = QChar::Null; return false; } /// Read next char *m_textStream >> m_nextChar; /// Test for new line if (m_nextChar == QLatin1Char('\n')) { /// Update variables tracking line numbers and line content ++m_lineNo; m_prevLine = m_currentLine; m_currentLine.clear(); } else { /// Add read char to current line m_currentLine.append(m_nextChar); } return true; } bool FileImporterBibTeX::readCharUntil(const QString &until) { Q_ASSERT_X(!until.isEmpty(), "bool FileImporterBibTeX::readCharUntil(const QString &until)", "\"until\" is empty or invalid"); bool result = true; while (!until.contains(m_nextChar) && (result = readChar())); return result; } bool FileImporterBibTeX::skipWhiteChar() { bool result = true; while ((m_nextChar.isSpace() || m_nextChar == QLatin1Char('\t') || m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r')) && result) result = readChar(); return result; } QString FileImporterBibTeX::readLine() { QString result; while (m_nextChar != QLatin1Char('\n') && m_nextChar != QLatin1Char('\r') && readChar()) result.append(m_nextChar); return result; } QList<QSharedPointer<Keyword> > FileImporterBibTeX::splitKeywords(const QString &text, char *usedSplitChar) { QList<QSharedPointer<Keyword> > result; static const QHash<char, QRegularExpression> splitAlong = { {'\n', QRegularExpression(QStringLiteral("\\s*\n\\s*"))}, {';', QRegularExpression(QStringLiteral("\\s*;\\s*"))}, {',', QRegularExpression(QString("\\s*,\\s*"))} }; if (usedSplitChar != nullptr) *usedSplitChar = '\0'; for (auto it = splitAlong.constBegin(); it != splitAlong.constEnd(); ++it) { /// check if character is contained in text (should be cheap to test) if (text.contains(QLatin1Char(it.key()))) { /// split text along a pattern like spaces-splitchar-spaces /// extract keywords static const QRegularExpression unneccessarySpacing(QStringLiteral("[ \n\r\t]+")); const QStringList keywords = text.split(it.value(), QString::SkipEmptyParts).replaceInStrings(unneccessarySpacing, QStringLiteral(" ")); /// build QList of Keyword objects from keywords for (const QString &keyword : keywords) { result.append(QSharedPointer<Keyword>(new Keyword(keyword))); } /// Memorize (some) split characters for later use /// (e.g. when writing file again) if (usedSplitChar != nullptr) *usedSplitChar = it.key(); /// no more splits necessary break; } } /// no split was performed, so whole text must be a single keyword if (result.isEmpty()) result.append(QSharedPointer<Keyword>(new Keyword(text))); return result; } QList<QSharedPointer<Person> > FileImporterBibTeX::splitNames(const QString &text, const int line_number, QObject *parent) { /// Case: Smith, John and Johnson, Tim /// Case: Smith, John and Fulkerson, Ford and Johnson, Tim /// Case: Smith, John, Fulkerson, Ford, and Johnson, Tim /// Case: John Smith and Tim Johnson /// Case: John Smith and Ford Fulkerson and Tim Johnson /// Case: Smith, John, Johnson, Tim /// Case: Smith, John, Fulkerson, Ford, Johnson, Tim /// Case: John Smith, Tim Johnson /// Case: John Smith, Tim Johnson, Ford Fulkerson /// Case: Smith, John ; Johnson, Tim ; Fulkerson, Ford (IEEE Xplore) /// German case: Robert A. Gehring und Bernd Lutterbeck QString internalText = text; /// Remove invalid characters such as dots or (double) daggers for footnotes static const QList<QChar> invalidChars {QChar(0x00b7), QChar(0x2020), QChar(0x2217), QChar(0x2021), QChar(0x002a), QChar(0x21d1) /** Upwards double arrow */}; for (const auto &invalidChar : invalidChars) /// Replacing daggers with commas ensures that they act as persons' names separator internalText = internalText.replace(invalidChar, QChar(',')); /// Remove numbers to footnotes static const QRegularExpression numberFootnoteRegExp(QStringLiteral("(\\w)\\d+\\b")); internalText = internalText.replace(numberFootnoteRegExp, QStringLiteral("\\1")); /// Remove academic degrees static const QRegularExpression academicDegreesRegExp(QStringLiteral("(,\\s*)?(MA|PhD)\\b")); internalText = internalText.remove(academicDegreesRegExp); /// Remove email addresses static const QRegularExpression emailAddressRegExp(QStringLiteral("\\b[a-zA-Z0-9][a-zA-Z0-9._-]+[a-zA-Z0-9]@[a-z0-9][a-z0-9-]*([.][a-z0-9-]+)*([.][a-z]+)+\\b")); internalText = internalText.remove(emailAddressRegExp); /// Split input string into tokens which are either name components (first or last name) /// or full names (composed of first and last name), depending on the input string's structure static const QRegularExpression split(QStringLiteral("\\s*([,]+|[,]*\\b[au]nd\\b|[;]|&|\\n|\\s{4,})\\s*")); const QStringList authorTokenList = internalText.split(split, QString::SkipEmptyParts); bool containsSpace = true; for (QStringList::ConstIterator it = authorTokenList.constBegin(); containsSpace && it != authorTokenList.constEnd(); ++it) containsSpace = (*it).contains(QChar(' ')); QList<QSharedPointer<Person> > result; result.reserve(authorTokenList.size()); if (containsSpace) { /// Tokens look like "John Smith" for (const QString &authorToken : authorTokenList) { QSharedPointer<Person> person = personFromString(authorToken, nullptr, line_number, parent); if (!person.isNull()) result.append(person); } } else { /// Tokens look like "Smith" or "John" /// Assumption: two consecutive tokens form a name for (QStringList::ConstIterator it = authorTokenList.constBegin(); it != authorTokenList.constEnd(); ++it) { QString lastname = *it; ++it; if (it != authorTokenList.constEnd()) { lastname += QStringLiteral(", ") + (*it); QSharedPointer<Person> person = personFromString(lastname, nullptr, line_number, parent); if (!person.isNull()) result.append(person); } else break; } } return result; } void FileImporterBibTeX::parsePersonList(const QString &text, Value &value, const int line_number, QObject *parent) { parsePersonList(text, value, nullptr, line_number, parent); } void FileImporterBibTeX::parsePersonList(const QString &text, Value &value, CommaContainment *comma, const int line_number, QObject *parent) { static const QString tokenAnd = QStringLiteral("and"); static const QString tokenOthers = QStringLiteral("others"); static QStringList tokens; contextSensitiveSplit(text, tokens); if (tokens.count() > 0) { if (tokens[0] == tokenAnd) { qCInfo(LOG_KBIBTEX_IO) << "Person list starts with" << tokenAnd << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list starts with 'and' near line %1")).arg(line_number))); } else if (tokens.count() > 1 && tokens[tokens.count() - 1] == tokenAnd) { qCInfo(LOG_KBIBTEX_IO) << "Person list ends with" << tokenAnd << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list ends with 'and' near line %1")).arg(line_number))); } if (tokens[0] == tokenOthers) { qCInfo(LOG_KBIBTEX_IO) << "Person list starts with" << tokenOthers << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list starts with 'others' near line %1")).arg(line_number))); } else if (tokens[tokens.count() - 1] == tokenOthers && (tokens.count() < 3 || tokens[tokens.count() - 2] != tokenAnd)) { qCInfo(LOG_KBIBTEX_IO) << "Person list ends with" << tokenOthers << "but is not preceeded with name and" << tokenAnd << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list ends with 'others' but is not preceeded with name and 'and' near line %1")).arg(line_number))); } } int nameStart = 0; QString prevToken; for (int i = 0; i < tokens.count(); ++i) { if (tokens[i] == tokenAnd) { if (prevToken == tokenAnd) { qCInfo(LOG_KBIBTEX_IO) << "Two subsequent" << tokenAnd << "found in person list near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Two subsequent 'and' found in person list near line %1")).arg(line_number))); } else if (nameStart < i) { const QSharedPointer<Person> person = personFromTokenList(tokens.mid(nameStart, i - nameStart), comma, line_number, parent); if (!person.isNull()) value.append(person); else { qCInfo(LOG_KBIBTEX_IO) << "Text" << tokens.mid(nameStart, i - nameStart).join(' ') << "does not form a name near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Text '%1' does not form a name near line %2")).arg(tokens.mid(nameStart, i - nameStart).join(' ')).arg(line_number))); } } else { qCInfo(LOG_KBIBTEX_IO) << "Found" << tokenAnd << "but no name before it near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Found 'and' but no name before it near line %1")).arg(line_number))); } nameStart = i + 1; } else if (tokens[i] == tokenOthers) { if (i < tokens.count() - 1) { qCInfo(LOG_KBIBTEX_IO) << "Special word" << tokenOthers << "found before last position in person name near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Special word 'others' found before last position in person name near line %1")).arg(line_number))); } else value.append(QSharedPointer<PlainText>(new PlainText(QStringLiteral("others")))); nameStart = tokens.count() + 1; } prevToken = tokens[i]; } if (nameStart < tokens.count()) { const QSharedPointer<Person> person = personFromTokenList(tokens.mid(nameStart), comma, line_number, parent); if (!person.isNull()) value.append(person); else { qCInfo(LOG_KBIBTEX_IO) << "Text" << tokens.mid(nameStart).join(' ') << "does not form a name near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Text '%1' does not form a name near line %2")).arg(tokens.mid(nameStart).join(' ')).arg(line_number))); } } } QSharedPointer<Person> FileImporterBibTeX::personFromString(const QString &name, const int line_number, QObject *parent) { return personFromString(name, nullptr, line_number, parent); } QSharedPointer<Person> FileImporterBibTeX::personFromString(const QString &name, CommaContainment *comma, const int line_number, QObject *parent) { static QStringList tokens; contextSensitiveSplit(name, tokens); return personFromTokenList(tokens, comma, line_number, parent); } QSharedPointer<Person> FileImporterBibTeX::personFromTokenList(const QStringList &tokens, CommaContainment *comma, const int line_number, QObject *parent) { if (comma != nullptr) *comma = ccNoComma; /// Simple case: provided list of tokens is empty, return invalid Person if (tokens.isEmpty()) return QSharedPointer<Person>(); /** * Sequence of tokens may contain somewhere a comma, like * "Tuckwell," "Peter". In this case, fill two string lists: * one with tokens before the comma, one with tokens after the * comma (excluding the comma itself). Example: * partA = ( "Tuckwell" ); partB = ( "Peter" ); partC = ( "Jr." ) * If a comma was found, boolean variable gotComma is set. */ QStringList partA, partB, partC; int commaCount = 0; for (const QString &token : tokens) { /// Position where comma was found, or -1 if no comma in token int p = -1; if (commaCount < 2) { /// Only check if token contains comma /// if no comma was found before int bracketCounter = 0; for (int i = 0; i < token.length(); ++i) { /// Consider opening curly brackets if (token[i] == QChar('{')) ++bracketCounter; /// Consider closing curly brackets else if (token[i] == QChar('}')) --bracketCounter; /// Only if outside any open curly bracket environments /// consider comma characters else if (bracketCounter == 0 && token[i] == QChar(',')) { /// Memorize comma's position and break from loop p = i; break; } else if (bracketCounter < 0) { /// Should never happen: more closing brackets than opening ones qCWarning(LOG_KBIBTEX_IO) << "Opening and closing brackets do not match near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Opening and closing brackets do not match near line %1")).arg(line_number))); } } } if (p >= 0) { if (commaCount == 0) { if (p > 0) partA.append(token.left(p)); if (p < token.length() - 1) partB.append(token.mid(p + 1)); } else if (commaCount == 1) { if (p > 0) partB.append(token.left(p)); if (p < token.length() - 1) partC.append(token.mid(p + 1)); } ++commaCount; } else if (commaCount == 0) partA.append(token); else if (commaCount == 1) partB.append(token); else if (commaCount == 2) partC.append(token); } if (commaCount > 0) { if (comma != nullptr) *comma = ccContainsComma; return QSharedPointer<Person>(new Person(partC.isEmpty() ? partB.join(QChar(' ')) : partC.join(QChar(' ')), partA.join(QChar(' ')), partC.isEmpty() ? QString() : partB.join(QChar(' ')))); } /** * PubMed uses a special writing style for names, where the * last name is followed by single capital letters, each being * the first letter of each first name. Example: Tuckwell P H * So, check how many single capital letters are at the end of * the given token list */ partA.clear(); partB.clear(); bool singleCapitalLetters = true; QStringList::ConstIterator it = tokens.constEnd(); while (it != tokens.constBegin()) { --it; if (singleCapitalLetters && it->length() == 1 && it->at(0).isUpper()) partB.prepend(*it); else { singleCapitalLetters = false; partA.prepend(*it); } } if (!partB.isEmpty()) { /// Name was actually given in PubMed format return QSharedPointer<Person>(new Person(partB.join(QChar(' ')), partA.join(QChar(' ')))); } /** * Normally, the last upper case token in a name is the last name * (last names consisting of multiple space-separated parts *have* * to be protected by {...}), but some languages have fill words * in lower case belonging to the last name as well (example: "van"). * In addition, some languages have capital case letters as well * (example: "Di Cosmo"). * Exception: Special keywords such as "Jr." can be appended to the * name, not counted as part of the last name. */ partA.clear(); partB.clear(); partC.clear(); static const QSet<QString> capitalCaseLastNameFragments {QStringLiteral("Di")}; it = tokens.constEnd(); while (it != tokens.constBegin()) { --it; if (partB.isEmpty() && (it->toLower().startsWith(QStringLiteral("jr")) || it->toLower().startsWith(QStringLiteral("sr")) || it->toLower().startsWith(QStringLiteral("iii")))) /// handle name suffices like "Jr" or "III." partC.prepend(*it); else if (partB.isEmpty() || it->at(0).isLower() || capitalCaseLastNameFragments.contains(*it)) partB.prepend(*it); else partA.prepend(*it); } if (!partB.isEmpty()) { /// Name was actually like "Peter Ole van der Tuckwell", /// split into "Peter Ole" and "van der Tuckwell" return QSharedPointer<Person>(new Person(partA.join(QChar(' ')), partB.join(QChar(' ')), partC.isEmpty() ? QString() : partC.join(QChar(' ')))); } qCWarning(LOG_KBIBTEX_IO) << "Don't know how to handle name" << tokens.join(QLatin1Char(' ')) << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Don't know how to handle name '%1' near line %2")).arg(tokens.join(QLatin1Char(' '))).arg(line_number))); return QSharedPointer<Person>(); } void FileImporterBibTeX::contextSensitiveSplit(const QString &text, QStringList &segments) { int bracketCounter = 0; ///< keep track of opening and closing brackets: {...} QString buffer; int len = text.length(); segments.clear(); ///< empty list for results before proceeding for (int pos = 0; pos < len; ++pos) { if (text[pos] == '{') ++bracketCounter; else if (text[pos] == '}') --bracketCounter; if (text[pos].isSpace() && bracketCounter == 0) { if (!buffer.isEmpty()) { segments.append(buffer); buffer.clear(); } } else buffer.append(text[pos]); } if (!buffer.isEmpty()) segments.append(buffer); } QString FileImporterBibTeX::bibtexAwareSimplify(const QString &text) { QString result; int i = 0; /// Consume initial spaces ... while (i < text.length() && text[i].isSpace()) ++i; /// ... but if there have been spaces (i.e. i>0), then record a single space only if (i > 0) result.append(QStringLiteral(" ")); while (i < text.length()) { /// Consume non-spaces while (i < text.length() && !text[i].isSpace()) { result.append(text[i]); ++i; } /// String may end with a non-space if (i >= text.length()) break; /// Consume spaces, ... while (i < text.length() && text[i].isSpace()) ++i; /// ... but record only a single space result.append(QStringLiteral(" ")); } return result; } bool FileImporterBibTeX::evaluateParameterComments(QTextStream *textStream, const QString &line, File *file) { /// Assertion: variable "line" is all lower-case /** check if this file requests a special encoding */ if (line.startsWith(QStringLiteral("@comment{x-kbibtex-encoding=")) && line.endsWith(QLatin1Char('}'))) { QString encoding = line.mid(28, line.length() - 29); textStream->setCodec(encoding == QStringLiteral("latex") ? defaultCodecName : encoding.toLatin1().data()); file->setProperty(File::Encoding, encoding == QStringLiteral("latex") ? encoding : textStream->codec()->name()); return true; } else if (line.startsWith(QStringLiteral("@comment{x-kbibtex-personnameformatting=")) && line.endsWith(QLatin1Char('}'))) { // TODO usage of x-kbibtex-personnameformatting is deprecated, // as automatic detection is in place QString personNameFormatting = line.mid(40, line.length() - 41); file->setProperty(File::NameFormatting, personNameFormatting); return true; } else if (line.startsWith(QStringLiteral("% encoding:"))) { /// Interprete JabRef's encoding information QString encoding = line.mid(12); qCDebug(LOG_KBIBTEX_IO) << "Using JabRef's encoding:" << encoding; textStream->setCodec(encoding.toLatin1()); encoding = textStream->codec()->name(); file->setProperty(File::Encoding, encoding); return true; } return false; } QString FileImporterBibTeX::tokenidToString(Token token) { switch (token) { case tAt: return QString(QStringLiteral("At")); case tBracketClose: return QString(QStringLiteral("BracketClose")); case tBracketOpen: return QString(QStringLiteral("BracketOpen")); case tAlphaNumText: return QString(QStringLiteral("AlphaNumText")); case tAssign: return QString(QStringLiteral("Assign")); case tComma: return QString(QStringLiteral("Comma")); case tDoublecross: return QString(QStringLiteral("Doublecross")); case tEOF: return QString(QStringLiteral("EOF")); case tUnknown: return QString(QStringLiteral("Unknown")); default: return QString(QStringLiteral("<Unknown>")); } } void FileImporterBibTeX::setCommentHandling(CommentHandling commentHandling) { m_commentHandling = commentHandling; } diff --git a/src/networking/onlinesearch/onlinesearchgooglescholar.cpp b/src/networking/onlinesearch/onlinesearchgooglescholar.cpp index e9660430..fdbfc53a 100644 --- a/src/networking/onlinesearch/onlinesearchgooglescholar.cpp +++ b/src/networking/onlinesearch/onlinesearchgooglescholar.cpp @@ -1,469 +1,469 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "onlinesearchgooglescholar.h" #include <QNetworkReply> #include <QIcon> #include <QUrlQuery> #include <QRegularExpression> #include <QTimer> #ifdef HAVE_KF5 #include <KLocalizedString> #else // HAVE_KF5 #define i18n(text) QObject::tr(text) #endif // HAVE_KF5 #include "fileimporterbibtex.h" #include "internalnetworkaccessmanager.h" #include "logging_networking.h" class OnlineSearchGoogleScholar::OnlineSearchGoogleScholarPrivate { public: int numResults; QMap<QString, QPair<QString, QString>> listBibTeXurls; QString queryFreetext, queryAuthor, queryYear; QString startPageUrl; QString advancedSearchPageUrl; QString queryPageUrl; FileImporterBibTeX *importer; OnlineSearchGoogleScholarPrivate(OnlineSearchGoogleScholar *parent) : numResults(0) { importer = new FileImporterBibTeX(parent); startPageUrl = QStringLiteral("http://scholar.google.com/"); queryPageUrl = QStringLiteral("http://%1/scholar"); } ~OnlineSearchGoogleScholarPrivate() { delete importer; } QString documentUrlForBibTeXEntry(const QString &htmlText, int bibLinkPos) { /// Regular expression to detect text of a link to a document static const QRegularExpression documentLinkIndicator(QStringLiteral("\\[(PDF|HTML)\\]"), QRegularExpression::CaseInsensitiveOption); /// Text for link is *before* the BibTeX link in Google's HTML code int posDocumentLinkText = htmlText.lastIndexOf(documentLinkIndicator, bibLinkPos); /// Check position of previous BibTeX link to not extract the wrong document link int posPreviousBib = htmlText.lastIndexOf(QStringLiteral("/scholar.bib"), bibLinkPos - 3); if (posPreviousBib < 0) posPreviousBib = 0; /// no previous BibTeX entry? /// If all found position values look reasonable ... if (posDocumentLinkText > posPreviousBib) { /// There is a [PDF] or [HTML] link for this BibTeX entry, so find URL /// Variables p1 and p2 are used to close in to the document's URL int p1 = htmlText.lastIndexOf(QStringLiteral("<a "), posDocumentLinkText); if (p1 > 0) { p1 = htmlText.indexOf(QStringLiteral("href=\""), p1); if (p1 > 0) { int p2 = htmlText.indexOf(QLatin1Char('"'), p1 + 7); if (p2 > 0) return htmlText.mid(p1 + 6, p2 - p1 - 6).replace(QStringLiteral("&amp;"), QStringLiteral("&")); } } } return QString(); } QString mainUrlForBibTeXEntry(const QString &htmlText, int bibLinkPos) { /// Text for link is *before* the BibTeX link in Google's HTML code int posH3 = htmlText.lastIndexOf(QStringLiteral("<h3 "), bibLinkPos); /// Check position of previous BibTeX link to not extract the wrong document link int posPreviousBib = htmlText.lastIndexOf(QStringLiteral("/scholar.bib"), bibLinkPos - 3); if (posPreviousBib < 0) posPreviousBib = 0; /// no previous BibTeX entry? /// If all found position values look reasonable ... if (posH3 > posPreviousBib) { /// There is a h3 tag for this BibTeX entry, so find URL /// Variables p1 and p2 are used to close in to the document's URL int p1 = htmlText.indexOf(QStringLiteral("href=\""), posH3); if (p1 > 0) { int p2 = htmlText.indexOf(QLatin1Char('"'), p1 + 7); if (p2 > 0) return htmlText.mid(p1 + 6, p2 - p1 - 6).replace(QStringLiteral("&amp;"), QStringLiteral("&")); } } return QString(); } }; OnlineSearchGoogleScholar::OnlineSearchGoogleScholar(QObject *parent) : OnlineSearchAbstract(parent), d(new OnlineSearchGoogleScholar::OnlineSearchGoogleScholarPrivate(this)) { /// nothing } OnlineSearchGoogleScholar::~OnlineSearchGoogleScholar() { delete d; } void OnlineSearchGoogleScholar::startSearch(const QMap<QString, QString> &query, int numResults) { d->numResults = numResults; m_hasBeenCanceled = false; emit progress(curStep = 0, numSteps = numResults + 4); const auto respectingQuotationMarksFreeText = splitRespectingQuotationMarks(query[queryKeyFreeText]); const auto respectingQuotationMarksTitle = splitRespectingQuotationMarks(query[queryKeyTitle]); QStringList queryFragments; queryFragments.reserve(respectingQuotationMarksFreeText.size() + respectingQuotationMarksTitle.size()); for (const QString &queryFragment : respectingQuotationMarksFreeText) { queryFragments.append(encodeURL(queryFragment)); } for (const QString &queryFragment : respectingQuotationMarksTitle) { queryFragments.append(encodeURL(queryFragment)); } d->queryFreetext = queryFragments.join(QStringLiteral("+")); const auto respectingQuotationMarksAuthor = splitRespectingQuotationMarks(query[queryKeyAuthor]); queryFragments.clear(); queryFragments.reserve(respectingQuotationMarksAuthor.size()); for (const QString &queryFragment : respectingQuotationMarksAuthor) { queryFragments.append(encodeURL(queryFragment)); } d->queryAuthor = queryFragments.join(QStringLiteral("+")); d->queryYear = encodeURL(query[queryKeyYear]); QUrl url(d->startPageUrl); QNetworkRequest request(url); QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(reply); connect(reply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingStartPage); refreshBusyProperty(); } void OnlineSearchGoogleScholar::doneFetchingStartPage() { emit progress(++curStep, numSteps); QNetworkReply *reply = static_cast<QNetworkReply *>(sender()); QUrl newDomainUrl; if (handleErrors(reply, newDomainUrl)) { if (newDomainUrl.isValid() && newDomainUrl != reply->url()) { /// following redirection to country-specific domain ++numSteps; QNetworkRequest request(newDomainUrl); QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(reply); connect(reply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingStartPage); } else { /// landed on country-specific domain static const QRegularExpression pathToSettingsPage(QStringLiteral(" href=\"(/scholar_settings[^ \"]*)")); const QString htmlCode = QString::fromUtf8(reply->readAll()); // dumpToFile(QStringLiteral("01-doneFetchingStartPage.html"),htmlCode); const QRegularExpressionMatch pathToSettingsPageMatch = pathToSettingsPage.match(htmlCode); if (!pathToSettingsPageMatch.hasMatch() || pathToSettingsPageMatch.captured(1).isEmpty()) { qCWarning(LOG_KBIBTEX_NETWORKING) << "No link to Google Scholar settings found"; stopSearch(resultNoError); return; } QUrl url = reply->url().resolved(QUrl(decodeURL(pathToSettingsPageMatch.captured(1)))); QUrlQuery query(url); query.removeQueryItem(QStringLiteral("hl")); query.addQueryItem(QStringLiteral("hl"), QStringLiteral("en")); query.removeQueryItem(QStringLiteral("as_sdt")); query.addQueryItem(QStringLiteral("as_sdt"), QStringLiteral("0,5")); url.setQuery(query); const QUrl replyUrl = reply->url(); - QTimer::singleShot(250, [this, url, replyUrl]() { + QTimer::singleShot(250, this, [this, url, replyUrl]() { QNetworkRequest request(url); QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request, replyUrl); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply); connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingConfigPage); }); } } refreshBusyProperty(); } void OnlineSearchGoogleScholar::doneFetchingConfigPage() { emit progress(++curStep, numSteps); QNetworkReply *reply = static_cast<QNetworkReply *>(sender()); QUrl redirUrl; if (handleErrors(reply, redirUrl)) { if (redirUrl.isValid()) { /// Redirection to another url ++numSteps; QNetworkRequest request(redirUrl); QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request, reply); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply); connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingConfigPage); } else { const QString htmlText = QString::fromUtf8(reply->readAll().constData()); // dumpToFile(QStringLiteral("02-doneFetchingConfigPage.html"), htmlText); static const QRegularExpression formOpeningTag(QStringLiteral("<form [^>]+action=\"([^\"]*scholar_setprefs[^\"]*)")); const QRegularExpressionMatch formOpeningTagMatch = formOpeningTag.match(htmlText); const int formOpeningTagPos = formOpeningTagMatch.capturedStart(0); if (formOpeningTagPos < 0) { qCWarning(LOG_KBIBTEX_NETWORKING) << "Could not find opening tag for form:" << formOpeningTag.pattern(); stopSearch(resultNoError); return; } QMap<QString, QString> inputMap = formParameters(htmlText, formOpeningTagPos); inputMap[QStringLiteral("hl")] = QStringLiteral("en"); inputMap[QStringLiteral("scis")] = QStringLiteral("yes"); inputMap[QStringLiteral("scisf")] = QStringLiteral("4"); inputMap[QStringLiteral("num")] = QString::number(d->numResults); inputMap[QStringLiteral("submit")] = QString(); QUrl url = reply->url().resolved(QUrl(decodeURL(formOpeningTagMatch.captured(1)))); QUrlQuery query(url); for (QMap<QString, QString>::ConstIterator it = inputMap.constBegin(); it != inputMap.constEnd(); ++it) { query.removeQueryItem(it.key()); query.addQueryItem(it.key(), it.value()); } url.setQuery(query); - QTimer::singleShot(250, [this, url, reply]() { + QTimer::singleShot(250, this, [this, url, reply]() { QNetworkRequest request(url); QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request, reply); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply); connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingSetConfigPage); }); } } refreshBusyProperty(); } void OnlineSearchGoogleScholar::doneFetchingSetConfigPage() { emit progress(++curStep, numSteps); QNetworkReply *reply = static_cast<QNetworkReply *>(sender()); QUrl redirUrl; if (handleErrors(reply, redirUrl)) { if (redirUrl.isValid()) { /// Redirection to another url ++numSteps; QNetworkRequest request(redirUrl); QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request, reply); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply); connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingSetConfigPage); } else { // const QString htmlText = QString::fromUtf8(reply->readAll().constData()); // dumpToFile(QStringLiteral("03-doneFetchingSetConfigPage.html"), htmlText); QUrl url(QString(d->queryPageUrl).arg(reply->url().host())); QUrlQuery query(url); query.addQueryItem(QStringLiteral("as_q"), d->queryFreetext); query.addQueryItem(QStringLiteral("as_sauthors"), d->queryAuthor); query.addQueryItem(QStringLiteral("as_ylo"), d->queryYear); query.addQueryItem(QStringLiteral("as_yhi"), d->queryYear); query.addQueryItem(QStringLiteral("as_vis"), QStringLiteral("1")); ///< include citations query.addQueryItem(QStringLiteral("num"), QString::number(d->numResults)); query.addQueryItem(QStringLiteral("btnG"), QStringLiteral("Search Scholar")); url.setQuery(query); - QTimer::singleShot(250, [this, url, reply]() { + QTimer::singleShot(250, this, [this, url, reply]() { QNetworkRequest request(url); QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request, reply); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply); connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingQueryPage); }); } } refreshBusyProperty(); } void OnlineSearchGoogleScholar::doneFetchingQueryPage() { emit progress(++curStep, numSteps); QNetworkReply *reply = static_cast<QNetworkReply *>(sender()); QUrl redirUrl; if (handleErrors(reply, redirUrl)) { if (redirUrl.isValid()) { /// Redirection to another url ++numSteps; QNetworkRequest request(redirUrl); QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request, reply); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply); connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingQueryPage); } else { const QString htmlText = QString::fromUtf8(reply->readAll().constData()); // dumpToFile(QStringLiteral("04-doneFetchingQueryPage.html"), htmlText); d->listBibTeXurls.clear(); #ifdef HAVE_KF5 if (htmlText.contains(QStringLiteral("enable JavaScript")) || htmlText.contains(QStringLiteral("re not a robot"))) { qCInfo(LOG_KBIBTEX_NETWORKING) << "'Google Scholar' denied scrapping data because it thinks KBibTeX is a robot."; sendVisualNotification(i18n("'Google Scholar' denied scrapping data because it thinks you are a robot."), label(), QStringLiteral("kbibtex"), 7 * 1000); } else { #endif // HAVE_KF5 static const QRegularExpression linkToBib("/scholar.bib\\?[^\" >]+"); QRegularExpressionMatchIterator linkToBibMatchIterator = linkToBib.globalMatch(htmlText); while (linkToBibMatchIterator.hasNext()) { const QRegularExpressionMatch linkToBibMatch = linkToBibMatchIterator.next(); const int pos = linkToBibMatch.capturedStart(); /// Try to figure out [PDF] or [HTML] link associated with BibTeX entry const QString documentUrl = d->documentUrlForBibTeXEntry(htmlText, pos); /// Extract primary link associated with BibTeX entry const QString primaryUrl = d->mainUrlForBibTeXEntry(htmlText, pos); const QString bibtexUrl(QStringLiteral("https://") + reply->url().host() + linkToBibMatch.captured().replace(QStringLiteral("&amp;"), QStringLiteral("&"))); d->listBibTeXurls.insert(bibtexUrl, qMakePair(primaryUrl, documentUrl)); } #ifdef HAVE_KF5 } #endif // HAVE_KF5 if (!d->listBibTeXurls.isEmpty()) { const auto listBibTeXurlsFront = d->listBibTeXurls.begin(); const QString bibtexUrl = listBibTeXurlsFront.key(); const QString primaryUrl = listBibTeXurlsFront.value().first; const QString documentUrl = listBibTeXurlsFront.value().second; - QTimer::singleShot(250, [this, bibtexUrl, primaryUrl, documentUrl, reply]() { + QTimer::singleShot(250, this, [this, bibtexUrl, primaryUrl, documentUrl, reply]() { QNetworkRequest request(bibtexUrl); QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request, reply); if (!primaryUrl.isEmpty()) { /// Store primary URL as a property of the request/reply newReply->setProperty("primaryurl", QVariant::fromValue<QString>(primaryUrl)); } if (!documentUrl.isEmpty()) { /// Store URL to document as a property of the request/reply newReply->setProperty("documenturl", QVariant::fromValue<QString>(documentUrl)); } InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply); connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingBibTeX); }); d->listBibTeXurls.erase(listBibTeXurlsFront); } else stopSearch(resultNoError); } } refreshBusyProperty(); } void OnlineSearchGoogleScholar::doneFetchingBibTeX() { emit progress(++curStep, numSteps); QNetworkReply *reply = static_cast<QNetworkReply *>(sender()); /// Extract previously stored URLs from reply const QString primaryUrl = reply->property("primaryurl").toString(); const QString documentUrl = reply->property("documenturl").toString(); QUrl newDomainUrl; if (handleErrors(reply, newDomainUrl)) { if (newDomainUrl.isValid() && newDomainUrl != reply->url()) { /// following redirection to country-specific domain ++numSteps; QNetworkRequest request(newDomainUrl); QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(reply); connect(reply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingBibTeX); } else { /// ensure proper treatment of UTF-8 characters const QString rawText = QString::fromUtf8(reply->readAll()); // dumpToFile(QStringLiteral("05-doneFetchingBibTeX.bib"),rawText); File *bibtexFile = d->importer->fromString(rawText); bool hasEntry = false; if (bibtexFile != nullptr) { for (const auto &element : const_cast<const File &>(*bibtexFile)) { QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (!entry.isNull()) { Value v; v.append(QSharedPointer<VerbatimText>(new VerbatimText(label()))); entry->insert(QStringLiteral("x-fetchedfrom"), v); if (!primaryUrl.isEmpty()) { /// There is an external document associated with this BibTeX entry Value urlValue = entry->value(Entry::ftUrl); urlValue.append(QSharedPointer<VerbatimText>(new VerbatimText(primaryUrl))); entry->insert(Entry::ftUrl, urlValue); } if (!documentUrl.isEmpty() && primaryUrl != documentUrl /** avoid duplicates */) { /// There is a web page associated with this BibTeX entry Value urlValue = entry->value(Entry::ftUrl); urlValue.append(QSharedPointer<VerbatimText>(new VerbatimText(documentUrl))); entry->insert(Entry::ftUrl, urlValue); } emit foundEntry(entry); hasEntry = true; } } delete bibtexFile; } if (!hasEntry) { qCWarning(LOG_KBIBTEX_NETWORKING) << "Searching" << label() << "resulted in invalid BibTeX data:" << rawText; stopSearch(resultUnspecifiedError); } else if (!d->listBibTeXurls.isEmpty()) { const auto listBibTeXurlsFront = d->listBibTeXurls.begin(); const QString bibtexUrl = listBibTeXurlsFront.key(); const QString primaryUrl = listBibTeXurlsFront.value().first; const QString documentUrl = listBibTeXurlsFront.value().second; QNetworkRequest request(bibtexUrl); QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request, reply); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply); if (!primaryUrl.isEmpty()) { /// Store primary URL as a property of the request/reply newReply->setProperty("primaryurl", QVariant::fromValue<QString>(primaryUrl)); } if (!documentUrl.isEmpty()) { /// Store URL to document as a property of the request/reply newReply->setProperty("documenturl", QVariant::fromValue<QString>(documentUrl)); } connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleScholar::doneFetchingBibTeX); d->listBibTeXurls.erase(listBibTeXurlsFront); } else stopSearch(resultNoError); } } refreshBusyProperty(); } QString OnlineSearchGoogleScholar::label() const { return i18n("Google Scholar"); } QString OnlineSearchGoogleScholar::favIconUrl() const { return QStringLiteral("https://scholar.google.com/favicon-png.ico"); } QUrl OnlineSearchGoogleScholar::homepage() const { return QUrl(QStringLiteral("https://scholar.google.com/")); }