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

0001 /*
0002     SPDX-FileCopyrightText: 2010 Yannick Motta <yannick.motta@gmail.com>
0003     SPDX-FileCopyrightText: 2010 Benjamin Port <port.benjamin@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "manpagedocumentation.h"
0009 
0010 #include "manpageplugin.h"
0011 #include "manpagedocumentationwidget.h"
0012 #include "debug.h"
0013 
0014 #include <documentation/standarddocumentationview.h>
0015 
0016 #include <KIO/TransferJob>
0017 #include <KLocalizedString>
0018 
0019 #include <QFile>
0020 #include <QHash>
0021 #include <QRegularExpression>
0022 #include <QStandardPaths>
0023 #include <QStringView>
0024 #include <QUrl>
0025 
0026 namespace {
0027 /**
0028  * This class makes sure that CSS embedded in man pages works and applies a custom style sheet on top.
0029  *
0030  * TODO: once Qt WebKit support is dropped, register with Qt WebEngine the "man" and "help" URL schemes as
0031  *       local; handle them. This will also make file:// links work properly. So this class would no longer
0032  *       have to fix embedded links and only need to embed our custom manpagedocumentation.css style like
0033  *       this: "<link href='file://%1' rel='stylesheet'>". Registering and handling the schemes might even
0034  *       allow to simplify the whole kdevmanpage plugin implementation.
0035  */
0036 class StyleSheetFixer
0037 {
0038 public:
0039     static void process(QString& htmlPage)
0040     {
0041         static StyleSheetFixer instance;
0042         instance.fix(htmlPage);
0043     }
0044 
0045 private:
0046     template <typename Location>
0047     static QString styleElementWithCode(const QByteArray& cssCode, const Location& location)
0048     {
0049         if (cssCode.isEmpty()) {
0050             qCWarning(MANPAGE) << "empty CSS file" << location;
0051             return QString();
0052         }
0053         return QString::fromUtf8("<style>" + cssCode + "</style>");
0054     }
0055 
0056     /**
0057      * Read the file contents and return it wrapped in a &lt;style&gt; HTML element.
0058      *
0059      * @return The &lt;style&gt; HTML element or an empty string in case of error.
0060      *
0061      * @note Referencing a local file via absolute path or file:// URL inside a &lt;link&gt;
0062      *       HTML element does not work because Qt WebEngine forbids such file system access.
0063      *       A comment under QTBUG-55902 proposes a workaround: pass "file://" as the baseUrl
0064      *       argument to QWebEnginePage::setHtml(). Unfortunately this base URL does not persist
0065      *       during back/forward web history navigation, so such navigation loads unstyled pages.
0066      */
0067     static QString readStyleSheet(const QString& fileName)
0068     {
0069         QFile file(fileName);
0070         if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0071             qCWarning(MANPAGE) << "cannot read CSS file" << fileName << ':' << file.error() << file.errorString();
0072             return QString();
0073         }
0074         const auto cssCode = file.readAll();
0075         return styleElementWithCode(cssCode, fileName);
0076     }
0077 
0078     /**
0079      * Get the URL contents and return it wrapped in a &lt;style&gt; HTML element.
0080      *
0081      * @return The &lt;style&gt; HTML element or an empty string in case of error.
0082      */
0083     static QString getStyleSheetContents(const QUrl& url)
0084     {
0085         auto* const job = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo);
0086         if (!job->exec()) {
0087             qCWarning(MANPAGE) << "couldn't get the contents of CSS file" << url << ':'
0088                                << job->error() << job->errorString();
0089             return QString();
0090         }
0091         const auto cssCode = job->data();
0092         return styleElementWithCode(cssCode, url);
0093     }
0094 
0095     static QString readCustomStyleSheet()
0096     {
0097         const auto customStyleSheetFile = QStringLiteral("kdevmanpage/manpagedocumentation.css");
0098         const QString cssFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, customStyleSheetFile);
0099         if (cssFilePath.isEmpty()) {
0100             qCWarning(MANPAGE) << "couldn't find" << customStyleSheetFile;
0101             return QString();
0102         }
0103         return readStyleSheet(cssFilePath);
0104     }
0105 
0106     StyleSheetFixer()
0107         : m_customStyleSheet{readCustomStyleSheet()}
0108     {
0109     }
0110 
0111     void fix(QString& htmlPage)
0112     {
0113         const QLatin1String headEndTag("</head>");
0114         const auto headEndTagPos = htmlPage.indexOf(headEndTag, 0, Qt::CaseInsensitive);
0115         if (headEndTagPos == -1) {
0116             qCWarning(MANPAGE) << "missing" << headEndTag << "on the HTML page.";
0117             return;
0118         }
0119 
0120         // Apply our custom style sheet to normalize look of the page. Embed the <style> element
0121         // into the HTML code directly rather than inject it with JavaScript to avoid reloading
0122         // and flickering of large pages such as cmake-modules man page.
0123         if (!m_customStyleSheet.isEmpty()) {
0124             htmlPage.insert(headEndTagPos, m_customStyleSheet);
0125         }
0126 
0127         expandUnsupportedLinks(htmlPage, headEndTagPos);
0128     }
0129 
0130     void expandUnsupportedLinks(QString& htmlPage, int endPos)
0131     {
0132         Q_ASSERT(endPos >= 0);
0133 
0134         static const QRegularExpression linkElement(QStringLiteral(R"(<link\s[^>]*rel="stylesheet"[^>]*>)"),
0135                                                     QRegularExpression::CaseInsensitiveOption);
0136         int startPos = 0;
0137         while (true) {
0138             const auto remainingPartOfThePage = QStringView{htmlPage}.mid(startPos, endPos - startPos);
0139             const auto linkElementMatch = linkElement.match(remainingPartOfThePage);
0140             if (!linkElementMatch.hasMatch()) {
0141                 break; // no more links to expand
0142             }
0143             startPos += linkElementMatch.capturedEnd();
0144 
0145             static const QRegularExpression hrefAttribute(QStringLiteral(R"|(\shref="([^"]*)")|"),
0146                                                           QRegularExpression::CaseInsensitiveOption);
0147             const auto hrefAttributeMatch = hrefAttribute.match(linkElementMatch.capturedView());
0148             if (!hrefAttributeMatch.hasMatch()) {
0149                 qCWarning(MANPAGE) << "missing href attribute in a stylesheet <link> element.";
0150                 continue;
0151             }
0152 
0153             const QUrl url{hrefAttributeMatch.captured(1)};
0154             const auto styleSheet = expandStyleSheet(url);
0155             if (styleSheet.isEmpty()) {
0156                 continue; // no code => skip this <link> element as expanding it won't make a difference
0157             }
0158 
0159             const auto linkElementLength = linkElementMatch.capturedLength();
0160             const auto linkElementPos = startPos - linkElementLength;
0161             htmlPage.replace(linkElementPos, linkElementLength, styleSheet);
0162 
0163             const auto htmlPageSizeIncrement = styleSheet.size() - linkElementLength;
0164             startPos += htmlPageSizeIncrement;
0165             endPos += htmlPageSizeIncrement;
0166         }
0167     }
0168 
0169     QString expandStyleSheet(const QUrl& url)
0170     {
0171         const bool isLocalFile = url.isLocalFile();
0172         const bool isHelpUrl = !isLocalFile && url.scheme() == QLatin1String{"help"};
0173         if (!isLocalFile && !isHelpUrl) {
0174             qCDebug(MANPAGE) << "not expanding CSS file URL with scheme" << url.scheme();
0175             return QString();
0176         }
0177 
0178         // Must do it this way because when an empty string is the value stored
0179         // for url, it should be returned rather than re-read from disk.
0180         const auto alreadyExpanded = m_expandedStyleSheets.constFind(url);
0181         if (alreadyExpanded != m_expandedStyleSheets.cend()) {
0182             return alreadyExpanded.value();
0183         }
0184 
0185         QString newlyExpanded;
0186         if (isLocalFile) {
0187             newlyExpanded = readStyleSheet(url.toLocalFile());
0188         } else {
0189             Q_ASSERT(isHelpUrl);
0190             // Neither Qt WebKit nor Qt WebEngine knows about the help protocol and URL scheme.
0191             // Expand the file contents at the help URL to apply the style sheet.
0192             newlyExpanded = getStyleSheetContents(url);
0193         }
0194 
0195         m_expandedStyleSheets.insert(url, newlyExpanded);
0196         return newlyExpanded;
0197     }
0198 
0199     /// The style sheet does not change => read it once and store in a constant.
0200     const QString m_customStyleSheet;
0201     /// Referenced style sheets should be few and rarely modified => read them once and store in this cache.
0202     QHash<QUrl, QString> m_expandedStyleSheets;
0203 };
0204 } // unnamed namespace
0205 
0206 ManPagePlugin* ManPageDocumentation::s_provider=nullptr;
0207 
0208 ManPageDocumentation::ManPageDocumentation(const QString& name, const QUrl& url)
0209     : m_url(url), m_name(name)
0210 {
0211     KIO::StoredTransferJob* transferJob = KIO::storedGet(m_url, KIO::NoReload, KIO::HideProgressInfo);
0212     connect( transferJob, &KIO::StoredTransferJob::finished, this, &ManPageDocumentation::finished);
0213     transferJob->start();
0214 }
0215 
0216 void ManPageDocumentation::finished(KJob* j)
0217 {
0218     auto* job = qobject_cast<KIO::StoredTransferJob*>(j);
0219     if(job && job->error()==0) {
0220         m_description = QString::fromUtf8(job->data());
0221         StyleSheetFixer::process(m_description);
0222     } else {
0223         m_description.clear();
0224     }
0225     emit descriptionChanged();
0226 }
0227 
0228 KDevelop::IDocumentationProvider* ManPageDocumentation::provider() const
0229 {
0230     return s_provider;
0231 }
0232 
0233 QString ManPageDocumentation::description() const
0234 {
0235     return m_description;
0236 }
0237 
0238 QWidget* ManPageDocumentation::documentationWidget(KDevelop::DocumentationFindWidget* findWidget, QWidget* parent )
0239 {
0240     auto* view = new KDevelop::StandardDocumentationView(findWidget, parent);
0241     view->initZoom(provider()->name());
0242     view->setDocumentation(IDocumentation::Ptr(this));
0243     view->setDelegateLinks(true);
0244     QObject::connect(view, &KDevelop::StandardDocumentationView::linkClicked, ManPageDocumentation::s_provider->model(), &ManPageModel::showItemFromUrl);
0245     return view;
0246 }
0247 
0248 bool ManPageDocumentation::providesWidget() const
0249 {
0250     return false;
0251 }
0252 
0253 QWidget* ManPageHomeDocumentation::documentationWidget(KDevelop::DocumentationFindWidget *findWidget, QWidget *parent){
0254     Q_UNUSED(findWidget);
0255     return new ManPageDocumentationWidget(parent);
0256 }
0257 
0258 
0259 QString ManPageHomeDocumentation::name() const
0260 {
0261     return i18n("Man Content Page");
0262 }
0263 
0264 KDevelop::IDocumentationProvider* ManPageHomeDocumentation::provider() const
0265 {
0266     return ManPageDocumentation::s_provider;
0267 }
0268 
0269 #include "moc_manpagedocumentation.cpp"