File indexing completed on 2024-05-05 04:40:50

0001 /*
0002     SPDX-FileCopyrightText: 2009 Aleix Pol <aleixpol@kde.org>
0003     SPDX-FileCopyrightText: 2009 David Nolden <david.nolden.kdevelop@art-master.de>
0004     SPDX-FileCopyrightText: 2010 Benjamin Port <port.benjamin@gmail.com>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "qthelpdocumentation.h"
0010 
0011 #include <QLabel>
0012 #include <QUrl>
0013 #include <QTreeView>
0014 #include <QHelpContentModel>
0015 #include <QHeaderView>
0016 #include <QMenu>
0017 #include <QMouseEvent>
0018 #include <QRegularExpression>
0019 #include <QActionGroup>
0020 
0021 #include <KLocalizedString>
0022 
0023 #include <interfaces/icore.h>
0024 #include <interfaces/idocumentationcontroller.h>
0025 #include <documentation/standarddocumentationview.h>
0026 #include "qthelpnetwork.h"
0027 #include "qthelpproviderabstract.h"
0028 
0029 #include <algorithm>
0030 
0031 using namespace KDevelop;
0032 
0033 QtHelpProviderAbstract* QtHelpDocumentation::s_provider=nullptr;
0034 
0035 QtHelpDocumentation::QtHelpDocumentation(const QString& name, const QList<QHelpLink>& info)
0036     : m_provider(s_provider)
0037     , m_name(name)
0038     , m_info(info)
0039     , m_current(info.constBegin())
0040     , lastView(nullptr)
0041 {
0042 }
0043 
0044 namespace {
0045 QList<QHelpLink>::const_iterator findTitle(const QList<QHelpLink>& links, const QString& title)
0046 {
0047     return std::find_if(links.begin(), links.end(), [title](const QHelpLink& helpLink) {
0048         return helpLink.title == title;
0049     });
0050 }
0051 }
0052 
0053 QtHelpDocumentation::QtHelpDocumentation(const QString& name, const QList<QHelpLink>& info, const QString& key)
0054     : m_provider(s_provider)
0055     , m_name(name)
0056     , m_info(info)
0057     , m_current(::findTitle(m_info, key))
0058     , lastView(nullptr)
0059 {
0060     Q_ASSERT(m_current!=m_info.constEnd());
0061 }
0062 
0063 QString QtHelpDocumentation::description() const
0064 {
0065     const QUrl url = currentUrl();
0066     //Extract a short description from the html data
0067     const QString dataString = QString::fromLatin1(m_provider->engine()->fileData(url)); ///@todo encoding
0068 
0069     const QString fragment = url.fragment();
0070     const QString p = QStringLiteral("((\\\")|(\\\'))");
0071     const QString optionalSpace = QStringLiteral(" *");
0072     const QString exp = QString(QLatin1String("< a name = ") + p + fragment + p + QLatin1String(" > < / a >")).replace(QLatin1Char(' '), optionalSpace);
0073 
0074     const QRegularExpression findFragment(exp);
0075     QRegularExpressionMatch findFragmentMatch;
0076     int pos = dataString.indexOf(findFragment, 0, &findFragmentMatch);
0077 
0078     if(fragment.isEmpty()) {
0079         pos = 0;
0080     } else {
0081 
0082         //Check if there is a title opening-tag right before the fragment, and if yes add it, so we have a nicely formatted caption
0083         const QString titleRegExp = QStringLiteral("< h\\d class = \".*\" >").replace(QLatin1Char(' '), optionalSpace);
0084         const QRegularExpression findTitle(titleRegExp);
0085         const QRegularExpressionMatch match = findTitle.match(dataString, pos);
0086         const int titleStart = match.capturedStart();
0087         const int titleEnd = titleStart + match.capturedEnd();
0088         if(titleStart != -1) {
0089             const QStringRef between = dataString.midRef(titleEnd, pos-titleEnd).trimmed();
0090             if(between.isEmpty())
0091                 pos = titleStart;
0092         }
0093     }
0094 
0095     if(pos != -1) {
0096         const QString exp = QString(QStringLiteral("< a name = ") + p + QStringLiteral("((\\S)*)") + p + QStringLiteral(" > < / a >")).replace(QLatin1Char(' '), optionalSpace);
0097         const QRegularExpression nextFragmentExpression(exp);
0098         int endPos = dataString.indexOf(nextFragmentExpression, pos+(fragment.size() ? findFragmentMatch.capturedLength() : 0));
0099         if(endPos == -1) {
0100             endPos = dataString.size();
0101         }
0102 
0103         {
0104             //Find the end of the last paragraph or newline, so we don't add prefixes of the following fragment
0105             const QString newLineRegExp = QStringLiteral ("< br / > | < / p >").replace(QLatin1Char(' '), optionalSpace);
0106             const QRegularExpression lastNewLine(newLineRegExp);
0107             QRegularExpressionMatch match;
0108             const int newEnd = dataString.lastIndexOf(lastNewLine, endPos, &match);
0109             if(match.isValid() && newEnd > pos)
0110                 endPos = newEnd + match.capturedLength();
0111         }
0112 
0113         {
0114             //Find the title, and start from there
0115             const QString titleRegExp = QStringLiteral("< h\\d class = \"title\" >").replace(QLatin1Char(' '), optionalSpace);
0116             const QRegularExpression findTitle(titleRegExp);
0117             const QRegularExpressionMatch match = findTitle.match(dataString);
0118             if (match.isValid())
0119                 pos = qBound(pos, match.capturedStart(), endPos);
0120         }
0121 
0122 
0123         QString thisFragment = dataString.mid(pos, endPos - pos);
0124 
0125         {
0126             //Completely remove the first large header found, since we don't need a header
0127             const QString headerRegExp = QStringLiteral("< h\\d.*>.*?< / h\\d >").replace(QLatin1Char(' '), optionalSpace);
0128             const QRegularExpression findHeader(headerRegExp);
0129             const QRegularExpressionMatch match = findHeader.match(thisFragment);
0130             if(match.isValid()) {
0131                 thisFragment.remove(match.capturedStart(), match.capturedLength());
0132             }
0133         }
0134 
0135         {
0136             //Replace all gigantic header-font sizes with <big>
0137             {
0138                 const QString sizeRegExp = QStringLiteral("< h\\d ").replace(QLatin1Char(' '), optionalSpace);
0139                 const QRegularExpression findSize(sizeRegExp);
0140                 thisFragment.replace(findSize, QStringLiteral("<big "));
0141             }
0142             {
0143                 const QString sizeCloseRegExp = QStringLiteral("< / h\\d >").replace(QLatin1Char(' '), optionalSpace);
0144                 const QRegularExpression closeSize(sizeCloseRegExp);
0145                 thisFragment.replace(closeSize, QStringLiteral("</big><br />"));
0146             }
0147         }
0148 
0149         {
0150             //Replace paragraphs by newlines
0151             const QString begin = QStringLiteral("< p >").replace(QLatin1Char(' '), optionalSpace);
0152             const QRegularExpression findBegin(begin);
0153             thisFragment.replace(findBegin, {});
0154 
0155             const QString end = QStringLiteral("< /p >").replace(QLatin1Char(' '), optionalSpace);
0156             const QRegularExpression findEnd(end);
0157             thisFragment.replace(findEnd, QStringLiteral("<br />"));
0158         }
0159 
0160         {
0161             //Remove links, because they won't work
0162             const QString link = QString(QStringLiteral("< a href = ") + p + QStringLiteral(".*?") + p).replace(QLatin1Char(' '), optionalSpace);
0163             const QRegularExpression exp(link, QRegularExpression::CaseInsensitiveOption);
0164             thisFragment.replace(exp, QStringLiteral("<a "));
0165         }
0166 
0167         return thisFragment;
0168     }
0169 
0170     QStringList titles;
0171     titles.reserve(m_info.size());
0172     for (auto& link : qAsConst(m_info)) {
0173         titles.append(link.title);
0174     }
0175     return titles.join(QLatin1String(", "));
0176 }
0177 
0178 QWidget* QtHelpDocumentation::documentationWidget(DocumentationFindWidget* findWidget, QWidget* parent)
0179 {
0180     if(m_info.isEmpty()) { //QtHelp sometimes has empty info maps. e.g. availableaudioeffects i 4.5.2
0181         return new QLabel(i18n("Could not find any documentation for '%1'", m_name), parent);
0182     } else {
0183         auto* view = new StandardDocumentationView(findWidget, parent);
0184         view->initZoom(m_provider->name());
0185         view->setDelegateLinks(true);
0186         view->setNetworkAccessManager(m_provider->networkAccess());
0187         view->setContextMenuPolicy(Qt::CustomContextMenu);
0188         QObject::connect(view, &StandardDocumentationView::linkClicked, this, &QtHelpDocumentation::jumpedTo);
0189         connect(view, &StandardDocumentationView::customContextMenuRequested, this, &QtHelpDocumentation::viewContextMenuRequested);
0190 
0191         view->load(currentUrl());
0192         lastView = view;
0193         return view;
0194     }
0195 }
0196 
0197 void QtHelpDocumentation::viewContextMenuRequested(const QPoint& pos)
0198 {
0199     auto* view = qobject_cast<StandardDocumentationView*>(sender());
0200     if (!view)
0201         return;
0202 
0203     auto menu = view->createStandardContextMenu();
0204 
0205     if (m_info.count() > 1) {
0206         if (!menu->isEmpty()) {
0207             menu->addSeparator();
0208         }
0209 
0210         auto* actionGroup = new QActionGroup(menu);
0211         for (auto it = m_info.constBegin(), end = m_info.constEnd(); it != end; ++it) {
0212             const QString& name = it->title;
0213             auto* act=new QtHelpAlternativeLink(name, this, actionGroup);
0214             act->setCheckable(true);
0215             act->setChecked(name==currentTitle());
0216             menu->addAction(act);
0217         }
0218     }
0219 
0220     menu->setAttribute(Qt::WA_DeleteOnClose);
0221     menu->exec(view->mapToGlobal(pos));
0222 }
0223 
0224 
0225 void QtHelpDocumentation::jumpedTo(const QUrl& newUrl)
0226 {
0227     Q_ASSERT(lastView);
0228     m_provider->jumpedTo(newUrl);
0229 }
0230 
0231 IDocumentationProvider* QtHelpDocumentation::provider() const
0232 {
0233     return m_provider;
0234 }
0235 
0236 QtHelpAlternativeLink::QtHelpAlternativeLink(const QString& name, const QtHelpDocumentation* doc, QObject* parent)
0237     : QAction(name, parent), mDoc(doc), mName(name)
0238 {
0239     connect(this, &QtHelpAlternativeLink::triggered, this, &QtHelpAlternativeLink::showUrl);
0240 }
0241 
0242 void QtHelpAlternativeLink::showUrl()
0243 {
0244     IDocumentation::Ptr newDoc(new QtHelpDocumentation(mName, mDoc->info(), mName));
0245     ICore::self()->documentationController()->showDocumentation(newDoc);
0246 }
0247 
0248 HomeDocumentation::HomeDocumentation() : m_provider(QtHelpDocumentation::s_provider)
0249 {
0250 }
0251 
0252 QWidget* HomeDocumentation::documentationWidget(DocumentationFindWidget*, QWidget* parent)
0253 {
0254     auto* w=new QTreeView(parent);
0255     // install an event filter to get the mouse events out of it
0256     w->viewport()->installEventFilter(this);
0257     w->header()->setVisible(false);
0258     w->setModel(m_provider->engine()->contentModel());
0259 
0260     connect(w, &QTreeView::clicked, this, &HomeDocumentation::clicked);
0261     return w;
0262 }
0263 
0264 void HomeDocumentation::clicked(const QModelIndex& idx)
0265 {
0266     QHelpContentModel* model = m_provider->engine()->contentModel();
0267     QHelpContentItem* it=model->contentItemAt(idx);
0268 
0269     const QList<QHelpLink> info{{it->url(), it->title()}};
0270     IDocumentation::Ptr newDoc(new QtHelpDocumentation(it->title(), info));
0271     ICore::self()->documentationController()->showDocumentation(newDoc);
0272 }
0273 
0274 QString HomeDocumentation::name() const
0275 {
0276     return i18n("QtHelp Home Page");
0277 }
0278 
0279 IDocumentationProvider* HomeDocumentation::provider() const
0280 {
0281     return m_provider;
0282 }
0283 
0284 bool HomeDocumentation::eventFilter(QObject* obj, QEvent* event)
0285 {
0286     if(event->type() == QEvent::MouseButtonPress) {
0287         // Here we need to set accpeted to false to let it propagate up
0288         event->setAccepted(false);
0289     }
0290     return QObject::eventFilter(obj, event);
0291 }
0292 
0293 #include "moc_qthelpdocumentation.cpp"