From fe7c446e5fb6218cdd73a02abb70d3d71932283d Mon Sep 17 00:00:00 2001 From: Le Tan Date: Mon, 26 Feb 2018 19:30:06 +0800 Subject: [PATCH] export: handle HTML resources --- src/dialog/vexportdialog.cpp | 11 ++- src/dialog/vexportdialog.h | 9 ++- src/resources/markdown_template.js | 41 +++++++++- src/utils/vutils.cpp | 25 ++++-- src/utils/vutils.h | 4 +- src/utils/vwebutils.cpp | 33 ++++++++ src/utils/vwebutils.h | 4 + src/vdownloader.cpp | 29 ++++++- src/vdownloader.h | 2 + src/vexporter.cpp | 118 ++++++++++++++++++++++++++--- src/vexporter.h | 18 +++++ 11 files changed, 272 insertions(+), 22 deletions(-) diff --git a/src/dialog/vexportdialog.cpp b/src/dialog/vexportdialog.cpp index 68f96652..376b4e7f 100644 --- a/src/dialog/vexportdialog.cpp +++ b/src/dialog/vexportdialog.cpp @@ -208,8 +208,16 @@ QWidget *VExportDialog::setupHTMLAdvancedSettings() m_embedStyleCB->setToolTip(tr("Embed CSS styles in HTML file")); m_embedStyleCB->setChecked(true); + // Complete HTML. + m_completeHTMLCB = new QCheckBox(tr("Complete page"), this); + m_completeHTMLCB->setToolTip(tr("Export the whole web page along with pictures " + "which may not keep the HTML link structure of " + "the original page")); + m_completeHTMLCB->setChecked(true); + QFormLayout *advLayout = new QFormLayout(); advLayout->addRow(m_embedStyleCB); + advLayout->addRow(m_completeHTMLCB); advLayout->setContentsMargins(0, 0, 0, 0); @@ -303,7 +311,8 @@ void VExportDialog::startExport() m_renderStyleCB->currentData().toString(), m_renderCodeBlockStyleCB->currentData().toString(), &m_pageLayout, - m_embedStyleCB->isChecked()); + m_embedStyleCB->isChecked(), + m_completeHTMLCB->isChecked()); s_lastExportFormat = opt.m_format; diff --git a/src/dialog/vexportdialog.h b/src/dialog/vexportdialog.h index e37607a3..e91629f8 100644 --- a/src/dialog/vexportdialog.h +++ b/src/dialog/vexportdialog.h @@ -47,7 +47,8 @@ struct ExportOption const QString &p_renderStyle, const QString &p_renderCodeBlockStyle, QPageLayout *p_layout, - bool p_embedCssStyle) + bool p_embedCssStyle, + bool p_completeHTML) : m_source(p_source), m_format(p_format), m_renderer(p_renderer), @@ -55,7 +56,8 @@ struct ExportOption m_renderStyle(p_renderStyle), m_renderCodeBlockStyle(p_renderCodeBlockStyle), m_layout(p_layout), - m_embedCssStyle(p_embedCssStyle) + m_embedCssStyle(p_embedCssStyle), + m_completeHTML(p_completeHTML) { } @@ -71,6 +73,7 @@ struct ExportOption QPageLayout *m_layout; bool m_embedCssStyle; + bool m_completeHTML; }; @@ -186,6 +189,8 @@ private: QCheckBox *m_embedStyleCB; + QCheckBox *m_completeHTMLCB;; + VNotebook *m_notebook; VDirectory *m_directory; diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index e0156925..7cff65ab 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -40,19 +40,56 @@ if (typeof VEnableImageCaption == 'undefined') { VEnableImageCaption = false; } +var getUrlScheme = function(url) { + var idx = url.indexOf(':'); + if (idx > -1) { + return url.substr(0, idx); + } else { + return null; + } +}; + +var replaceCssUrl = function(baseUrl, match, p1, offset, str) { + if (getUrlScheme(p1)) { + return match; + } + + var url = baseUrl + '/' + p1; + return "url(\"" + url + "\");"; +}; + +var translateCssUrlToAbsolute = function(baseUrl, css) { + return css.replace(/\burl\(\"([^\"\)]+)\"\);/g, replaceCssUrl.bind(undefined, baseUrl)); +}; + var styleContent = function() { var styles = ""; for (var i = 0; i < document.styleSheets.length; ++i) { var styleSheet = document.styleSheets[i]; if (styleSheet.cssRules) { + var baseUrl = null; + if (styleSheet.href) { + var scheme = getUrlScheme(styleSheet.href); + // We only translate local resources. + if (scheme == 'file' || scheme == 'qrc') { + baseUrl = styleSheet.href.substr(0, styleSheet.href.lastIndexOf('/')); + } + } + for (var j = 0; j < styleSheet.cssRules.length; ++j) { - styles = styles + styleSheet.cssRules[j].cssText + "\n"; + var css = styleSheet.cssRules[j].cssText; + if (baseUrl) { + // Try to replace the url() with absolute path. + css = translateCssUrlToAbsolute(baseUrl, css); + } + + styles = styles + css + "\n"; } } } return styles; -} +}; var htmlContent = function() { content.htmlContentCB("", styleContent(), placeholder.innerHTML); diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 2a7d23e6..7efa0806 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -74,17 +74,32 @@ QString VUtils::readFileFromDisk(const QString &filePath) return fileText; } -bool VUtils::writeFileToDisk(const QString &filePath, const QString &text) +bool VUtils::writeFileToDisk(const QString &p_filePath, const QString &p_text) { - QFile file(filePath); + QFile file(p_filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - qWarning() << "fail to open file" << filePath << "to write"; + qWarning() << "fail to open file" << p_filePath << "to write"; return false; } + QTextStream stream(&file); - stream << text; + stream << p_text; file.close(); - qDebug() << "write file content:" << filePath; + qDebug() << "write file content:" << p_filePath; + return true; +} + +bool VUtils::writeFileToDisk(const QString &p_filePath, const QByteArray &p_data) +{ + QFile file(p_filePath); + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "fail to open file" << p_filePath << "to write"; + return false; + } + + file.write(p_data); + file.close(); + qDebug() << "write file content:" << p_filePath; return true; } diff --git a/src/utils/vutils.h b/src/utils/vutils.h index fa2097b1..6e95f5dc 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -80,7 +80,9 @@ class VUtils public: static QString readFileFromDisk(const QString &filePath); - static bool writeFileToDisk(const QString &filePath, const QString &text); + static bool writeFileToDisk(const QString &p_filePath, const QString &p_text); + + static bool writeFileToDisk(const QString &p_filePath, const QByteArray &p_data); static bool writeJsonToDisk(const QString &p_filePath, const QJsonObject &p_json); diff --git a/src/utils/vwebutils.cpp b/src/utils/vwebutils.cpp index b16be44e..addf6111 100644 --- a/src/utils/vwebutils.cpp +++ b/src/utils/vwebutils.cpp @@ -3,9 +3,13 @@ #include #include #include +#include +#include #include "vpalette.h" #include "vconfigmanager.h" +#include "utils/vutils.h" +#include "vdownloader.h" extern VPalette *g_palette; @@ -891,3 +895,32 @@ bool VWebUtils::fixXHtmlTags(QString &p_html) return altered; } + +QString VWebUtils::copyResource(const QUrl &p_url, const QString &p_folder) const +{ + Q_ASSERT(!p_url.isRelative()); + + QDir dir(p_folder); + if (!dir.exists()) { + VUtils::makePath(p_folder); + } + + QString file = p_url.isLocalFile() ? p_url.toLocalFile() : p_url.toString(); + QString fileName = VUtils::fileNameFromPath(file); + fileName = VUtils::getFileNameWithSequence(p_folder, fileName, true); + QString targetFile = dir.absoluteFilePath(fileName); + + bool succ = false; + if (p_url.scheme() == "https" || p_url.scheme() == "http") { + // Download it. + QByteArray data = VDownloader::downloadSync(p_url); + if (!data.isEmpty()) { + succ = VUtils::writeFileToDisk(targetFile, data); + } + } else if (QFileInfo::exists(file)) { + // Do a copy. + succ = VUtils::copyFile(file, targetFile, false); + } + + return succ ? targetFile : QString(); +} diff --git a/src/utils/vwebutils.h b/src/utils/vwebutils.h index 2bc5ea72..dd1a368b 100644 --- a/src/utils/vwebutils.h +++ b/src/utils/vwebutils.h @@ -21,6 +21,10 @@ public: // Returns true if @p_html is modified. bool alterHtmlAsTarget(const QUrl &p_baseUrl, QString &p_html, const QString &p_target) const; + // Download or copy @p_url to @p_folder. + // Return the target file path on success or empty string on failure. + QString copyResource(const QUrl &p_url, const QString &p_folder) const; + private: struct CopyTargetAction { diff --git a/src/vdownloader.cpp b/src/vdownloader.cpp index 8f6ec98d..ec7666d0 100644 --- a/src/vdownloader.cpp +++ b/src/vdownloader.cpp @@ -1,5 +1,7 @@ #include "vdownloader.h" +#include "utils/vutils.h" + VDownloader::VDownloader(QObject *parent) : QObject(parent) { @@ -11,7 +13,6 @@ void VDownloader::handleDownloadFinished(QNetworkReply *reply) { data = reply->readAll(); reply->deleteLater(); - qDebug() << "VDownloader receive" << reply->url().toString(); emit downloadFinished(data, reply->url().toString()); } @@ -23,5 +24,29 @@ void VDownloader::download(const QUrl &p_url) QNetworkRequest request(p_url); webCtrl.get(request); - qDebug() << "VDownloader get" << p_url.toString(); +} + +QByteArray VDownloader::downloadSync(const QUrl &p_url) +{ + QByteArray data; + if (!p_url.isValid()) { + return data; + } + + bool finished = false; + QNetworkAccessManager nam; + connect(&nam, &QNetworkAccessManager::finished, + [&data, &finished](QNetworkReply *p_reply) { + data = p_reply->readAll(); + p_reply->deleteLater(); + finished = true; + }); + + nam.get(QNetworkRequest(p_url)); + + while (!finished) { + VUtils::sleepWait(100); + } + + return data; } diff --git a/src/vdownloader.h b/src/vdownloader.h index 31e20877..3a48c388 100644 --- a/src/vdownloader.h +++ b/src/vdownloader.h @@ -15,6 +15,8 @@ public: explicit VDownloader(QObject *parent = 0); void download(const QUrl &p_url); + static QByteArray downloadSync(const QUrl &p_url); + signals: void downloadFinished(const QByteArray &data, const QString &url); diff --git a/src/vexporter.cpp b/src/vexporter.cpp index 7387ffd7..914323e4 100644 --- a/src/vexporter.cpp +++ b/src/vexporter.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "vconfigmanager.h" #include "vfile.h" @@ -12,9 +13,12 @@ #include "vconstants.h" #include "vmarkdownconverter.h" #include "vdocument.h" +#include "utils/vwebutils.h" extern VConfigManager *g_config; +extern VWebUtils *g_webUtils; + VExporter::VExporter(QWidget *p_parent) : QObject(p_parent), m_webViewer(NULL), @@ -82,7 +86,8 @@ void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt) m_webDocument->setHtml(html); } - m_webViewer->setHtml(m_htmlTemplate, p_file->getBaseUrl()); + m_baseUrl = p_file->getBaseUrl(); + m_webViewer->setHtml(m_htmlTemplate, m_baseUrl); } void VExporter::handleLogicsFinished() @@ -107,6 +112,7 @@ void VExporter::clearWebViewer() delete m_webViewer; m_webViewer = NULL; m_webDocument = NULL; + m_baseUrl.clear(); } bool VExporter::exportToPDF(VWebView *p_webViewer, @@ -122,16 +128,11 @@ bool VExporter::exportToPDF(VWebView *p_webViewer, V_ASSERT(!p_filePath.isEmpty()); - QFile file(p_filePath); - - if (!file.open(QFile::WriteOnly)) { + if (!VUtils::writeFileToDisk(p_filePath, p_result)) { pdfPrinted = -1; return; } - file.write(p_result.data(), p_result.size()); - file.close(); - pdfPrinted = 1; }, p_layout); @@ -201,6 +202,7 @@ bool VExporter::exportViaWebView(VFile *p_file, exportRet = exportToHTML(m_webViewer, m_webDocument, p_opt.m_embedCssStyle, + p_opt.m_completeHTML, p_outputFile); break; @@ -237,6 +239,7 @@ exit: bool VExporter::exportToHTML(VWebView *p_webViewer, VDocument *p_webDocument, bool p_embedCssStyle, + bool p_completeHTML, const QString &p_filePath) { Q_UNUSED(p_webViewer); @@ -260,20 +263,40 @@ bool VExporter::exportToHTML(VWebView *p_webViewer, return; } + QString resFolder = QFileInfo(p_filePath).completeBaseName() + "_files"; + QString resFolderPath = QDir(VUtils::basePathFromPath(p_filePath)).filePath(resFolder); + + qDebug() << "HTML files folder" << resFolderPath; + QString html(m_exportHtmlTemplate); if (!p_styleContent.isEmpty() && p_embedCssStyle) { - html.replace(HtmlHolder::c_styleHolder, p_styleContent); + QString content(p_styleContent); + fixStyleResources(resFolderPath, content); + html.replace(HtmlHolder::c_styleHolder, content); } if (!p_headContent.isEmpty()) { html.replace(HtmlHolder::c_headHolder, p_headContent); } - html.replace(HtmlHolder::c_bodyHolder, p_bodyContent); + if (p_completeHTML) { + QString content(p_bodyContent); + fixBodyResources(m_baseUrl, resFolderPath, content); + html.replace(HtmlHolder::c_bodyHolder, content); + } else { + html.replace(HtmlHolder::c_bodyHolder, p_bodyContent); + } file.write(html.toUtf8()); file.close(); + // Delete empty resource folder. + QDir dir(resFolderPath); + if (dir.isEmpty()) { + dir.cdUp(); + dir.rmdir(resFolder); + } + htmlExported = 1; }); @@ -289,3 +312,80 @@ bool VExporter::exportToHTML(VWebView *p_webViewer, return htmlExported == 1; } + +bool VExporter::fixStyleResources(const QString &p_folder, + QString &p_html) +{ + bool altered = false; + QRegExp reg("\\burl\\(\"((file|qrc):[^\"\\)]+)\"\\);"); + + int pos = 0; + while (pos < p_html.size()) { + int idx = p_html.indexOf(reg, pos); + if (idx == -1) { + break; + } + + QString targetFile = g_webUtils->copyResource(QUrl(reg.cap(1)), p_folder); + if (targetFile.isEmpty()) { + pos = idx + reg.matchedLength(); + } else { + // Replace the url string in html. + QString newUrl = QString("url(\"%1\");").arg(getResourceRelativePath(targetFile)); + p_html.replace(idx, reg.matchedLength(), newUrl); + pos = idx + newUrl.size(); + altered = true; + } + } + + return altered; +} + +bool VExporter::fixBodyResources(const QUrl &p_baseUrl, + const QString &p_folder, + QString &p_html) +{ + bool altered = false; + if (p_baseUrl.isEmpty()) { + return altered; + } + + QRegExp reg("]*)src=\"([^\"]+)\"([^>]*)>"); + + int pos = 0; + while (pos < p_html.size()) { + int idx = p_html.indexOf(reg, pos); + if (idx == -1) { + break; + } + + if (reg.cap(2).isEmpty()) { + pos = idx + reg.matchedLength(); + continue; + } + + QUrl srcUrl(p_baseUrl.resolved(reg.cap(2))); + QString targetFile = g_webUtils->copyResource(srcUrl, p_folder); + if (targetFile.isEmpty()) { + pos = idx + reg.matchedLength(); + } else { + // Replace the url string in html. + QString newUrl = QString("").arg(reg.cap(1)) + .arg(getResourceRelativePath(targetFile)) + .arg(reg.cap(3)); + p_html.replace(idx, reg.matchedLength(), newUrl); + pos = idx + newUrl.size(); + altered = true; + } + } + + return altered; +} + +QString VExporter::getResourceRelativePath(const QString &p_file) +{ + int idx = p_file.lastIndexOf('/'); + int idx2 = p_file.lastIndexOf('/', idx - 1); + Q_ASSERT(idx > 0 && idx2 < idx); + return "." + p_file.mid(idx2); +} diff --git a/src/vexporter.h b/src/vexporter.h index e8439ccf..08e72845 100644 --- a/src/vexporter.h +++ b/src/vexporter.h @@ -3,6 +3,7 @@ #include #include +#include #include "dialog/vexportdialog.h" @@ -75,8 +76,22 @@ private: bool exportToHTML(VWebView *p_webViewer, VDocument *p_webDocument, bool p_embedCssStyle, + bool p_completeHTML, const QString &p_filePath); + // Fix @p_html's resources like url("...") with "file" or "qrc" schema. + // Copy the resource to @p_folder and fix the url string. + static bool fixStyleResources(const QString &p_folder, + QString &p_html); + + // Fix @p_html's resources like . + // Copy the resource to @p_folder and fix the url string. + static bool fixBodyResources(const QUrl &p_baseUrl, + const QString &p_folder, + QString &p_html); + + static QString getResourceRelativePath(const QString &p_file); + QPageLayout m_pageLayout; // Will be allocated and free for each conversion. @@ -84,6 +99,9 @@ private: VDocument *m_webDocument; + // Base URL of VWebView. + QUrl m_baseUrl; + QString m_htmlTemplate; // Template to hold the export HTML result.