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 <style> HTML element. 0058 * 0059 * @return The <style> 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 <link> 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 <style> HTML element. 0080 * 0081 * @return The <style> 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"