From 1fe7567d79fe59f1843e39de407cff7d281a0459 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Sat, 24 Feb 2018 23:08:27 +0800 Subject: [PATCH] export: support HTML format --- src/dialog/vexportdialog.cpp | 41 ++++- src/dialog/vexportdialog.h | 5 + src/resources/export_template.html | 14 ++ src/resources/markdown-it.js | 4 +- src/resources/markdown_template.html | 2 +- src/resources/markdown_template.js | 32 +++- src/resources/simple_template.html | 2 +- src/resources/vnote.ini | 2 +- src/utils/vutils.cpp | 5 + src/utils/vutils.h | 3 + src/vconstants.h | 1 + src/vdocument.cpp | 10 ++ src/vdocument.h | 10 ++ src/vexporter.cpp | 222 ++++++++++++++++++--------- src/vexporter.h | 20 +++ src/vnote.cpp | 28 +++- src/vnote.h | 3 + src/vnote.qrc | 1 + 18 files changed, 323 insertions(+), 82 deletions(-) create mode 100644 src/resources/export_template.html diff --git a/src/dialog/vexportdialog.cpp b/src/dialog/vexportdialog.cpp index 6b2d80d6..e57c97c8 100644 --- a/src/dialog/vexportdialog.cpp +++ b/src/dialog/vexportdialog.cpp @@ -289,7 +289,8 @@ void VExportDialog::startExport() m_consoleEdit->clear(); appendLogLine(tr("Export to %1.").arg(outputFolder)); - if (opt.m_format == ExportFormat::PDF) { + if (opt.m_format == ExportFormat::PDF + || opt.m_format == ExportFormat::HTML) { m_exporter->prepareExport(opt); } @@ -406,6 +407,10 @@ int VExportDialog::doExport(VFile *p_file, ret = doExportPDF(p_file, p_opt, p_outputFolder, p_errMsg); break; + case (int)ExportFormat::HTML: + ret = doExportHTML(p_file, p_opt, p_outputFolder, p_errMsg); + break; + default: break; } @@ -628,6 +633,40 @@ int VExportDialog::doExportPDF(VFile *p_file, } } +int VExportDialog::doExportHTML(VFile *p_file, + const ExportOption &p_opt, + const QString &p_outputFolder, + QString *p_errMsg) +{ + Q_UNUSED(p_opt); + + QString srcFilePath(p_file->fetchPath()); + + if (p_file->getDocType() != DocType::Markdown) { + LOGERR(tr("Skip exporting non-Markdown file %1 as HTML.").arg(srcFilePath)); + return 0; + } + + if (!VUtils::makePath(p_outputFolder)) { + LOGERR(tr("Fail to create directory %1.").arg(p_outputFolder)); + return 0; + } + + // Get output file. + QString suffix = ".html"; + QString name = VUtils::getFileNameWithSequence(p_outputFolder, + QFileInfo(p_file->getName()).completeBaseName() + suffix); + QString outputPath = QDir(p_outputFolder).filePath(name); + + if (m_exporter->exportHTML(p_file, p_opt, outputPath, p_errMsg)) { + appendLogLine(tr("Note %1 exported to %2.").arg(srcFilePath).arg(outputPath)); + return 1; + } else { + appendLogLine(tr("Fail to export note %1.").arg(srcFilePath)); + return 0; + } +} + bool VExportDialog::checkUserAction() { if (m_askedToStop) { diff --git a/src/dialog/vexportdialog.h b/src/dialog/vexportdialog.h index 1c648098..24ec5a33 100644 --- a/src/dialog/vexportdialog.h +++ b/src/dialog/vexportdialog.h @@ -133,6 +133,11 @@ private: const QString &p_outputFolder, QString *p_errMsg = NULL); + int doExportHTML(VFile *p_file, + const ExportOption &p_opt, + const QString &p_outputFolder, + QString *p_errMsg = NULL); + // Return false if we could not continue. bool checkUserAction(); diff --git a/src/resources/export_template.html b/src/resources/export_template.html new file mode 100644 index 00000000..f52d39d3 --- /dev/null +++ b/src/resources/export_template.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/src/resources/markdown-it.js b/src/resources/markdown-it.js index b91a8b52..68417360 100644 --- a/src/resources/markdown-it.js +++ b/src/resources/markdown-it.js @@ -127,7 +127,7 @@ var updateText = function(text) { var highlightText = function(text, id, timeStamp) { var html = mdit.render(text); content.highlightTextCB(html, id, timeStamp); -} +}; var textToHtml = function(text) { var html = mdit.render(text); @@ -139,4 +139,4 @@ var textToHtml = function(text) { container.innerHTML = ""; content.textToHtmlCB(text, html); -} +}; diff --git a/src/resources/markdown_template.html b/src/resources/markdown_template.html index be804b3c..f7c91dbe 100644 --- a/src/resources/markdown_template.html +++ b/src/resources/markdown_template.html @@ -3,7 +3,7 @@ "; + return styles; +}; + +var htmlContent = function() { + content.htmlContentCB(headContent(), placeholder.innerHTML); +}; + new QWebChannel(qt.webChannelTransport, function(channel) { content = channel.objects.content; @@ -62,6 +82,10 @@ new QWebChannel(qt.webChannelTransport, content.requestTextToHtml.connect(textToHtml); content.noticeReadyToTextToHtml(); } + + if (typeof htmlContent == "function") { + content.requestHtmlContent.connect(htmlContent); + } }); var VHighlightedAnchorClass = 'highlighted-anchor'; @@ -447,7 +471,7 @@ var renderMermaidOne = function(code) { mermaidIdx++; try { // Do not increment mermaidIdx here. - var graph = mermaidAPI.render('mermaid-diagram-' + mermaidIdx, code.innerText, function(){}); + var graph = mermaidAPI.render('mermaid-diagram-' + mermaidIdx, code.textContent, function(){}); } catch (err) { content.setLog("err: " + err); return false; @@ -493,7 +517,7 @@ var renderFlowchartOne = function(code) { // Flowchart code block. flowchartIdx++; try { - var graph = flowchart.parse(code.innerText); + var graph = flowchart.parse(code.textContent); } catch (err) { content.setLog("err: " + err); return false; @@ -525,7 +549,7 @@ var renderFlowchartOne = function(code) { var isImageBlock = function(img) { var pn = img.parentNode; - return (pn.children.length == 1) && (pn.innerText == ''); + return (pn.children.length == 1) && (pn.textContent == ''); }; var isImageWithBr = function(img) { @@ -617,7 +641,7 @@ var insertImageCaption = function() { // Add caption. var captionDiv = document.createElement('div'); captionDiv.classList.add(VImageCaptionClass); - captionDiv.innerText = img.alt; + captionDiv.textContent = img.alt; img.insertAdjacentElement('afterend', captionDiv); } } diff --git a/src/resources/simple_template.html b/src/resources/simple_template.html index 165cde37..1f970dca 100644 --- a/src/resources/simple_template.html +++ b/src/resources/simple_template.html @@ -6,6 +6,6 @@ - + diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index b58f9246..7bbad124 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -203,7 +203,7 @@ enable_wildcard_in_simple_search=true [web] ; Location and configuration for Mathjax -mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML +mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_HTMLorMML ; Styles to be removed when copied ; style1,style2,style3 diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 94f9bfe5..5e6377b2 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -690,6 +690,11 @@ QString VUtils::generateHtmlTemplate(const QString &p_template, return htmlTemplate; } +QString VUtils::generateExportHtmlTemplate(const QString &p_renderBg) +{ + return VNote::generateExportHtmlTemplate(g_config->getRenderBackgroundColor(p_renderBg)); +} + QString VUtils::getFileNameWithSequence(const QString &p_directory, const QString &p_baseFileName, bool p_completeBaseName) diff --git a/src/utils/vutils.h b/src/utils/vutils.h index 21e97169..fa2097b1 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -174,6 +174,9 @@ public: const QString &p_renderCodeBlockStyle, bool p_isPDF); + // @p_renderBg is the background name. + static QString generateExportHtmlTemplate(const QString &p_renderBg); + static QString generateSimpleHtmlTemplate(const QString &p_body); // Get an available file name in @p_directory with base @p_baseFileName. diff --git a/src/vconstants.h b/src/vconstants.h index 1a034d27..60aee913 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -37,6 +37,7 @@ namespace HtmlHolder static const QString c_JSHolder = "JS_PLACE_HOLDER"; static const QString c_extraHolder = ""; static const QString c_bodyHolder = ""; + static const QString c_headHolder = ""; } // Directory Config file items. diff --git a/src/vdocument.cpp b/src/vdocument.cpp index cee4abad..3240f146 100644 --- a/src/vdocument.cpp +++ b/src/vdocument.cpp @@ -82,6 +82,11 @@ void VDocument::textToHtmlAsync(const QString &p_text) emit requestTextToHtml(p_text); } +void VDocument::getHtmlContentAsync() +{ + emit requestHtmlContent(); +} + void VDocument::textToHtmlCB(const QString &p_text, const QString &p_html) { emit textToHtmlFinished(p_text, p_html); @@ -108,3 +113,8 @@ void VDocument::finishLogics() qDebug() << "Web side finished logics"; emit logicsFinished(); } + +void VDocument::htmlContentCB(const QString &p_head, const QString &p_body) +{ + emit htmlContentFinished(p_head, p_body); +} diff --git a/src/vdocument.h b/src/vdocument.h index d6880288..fb791ea9 100644 --- a/src/vdocument.h +++ b/src/vdocument.h @@ -38,6 +38,9 @@ public: bool isReadyToTextToHtml() const; + // Request to get the HTML content. + void getHtmlContentAsync(); + public slots: // Will be called in the HTML side @@ -67,6 +70,8 @@ public slots: // But the page may not finish loading, such as images. void finishLogics(); + void htmlContentCB(const QString &p_head, const QString &p_body); + signals: void textChanged(const QString &text); @@ -95,6 +100,11 @@ signals: void textToHtmlFinished(const QString &p_text, const QString &p_html); + void requestHtmlContent(); + + void htmlContentFinished(const QString &p_headContent, + const QString &p_bodyContent); + private: QString m_toc; QString m_header; diff --git a/src/vexporter.cpp b/src/vexporter.cpp index 0095483d..b8e5ec6e 100644 --- a/src/vexporter.cpp +++ b/src/vexporter.cpp @@ -29,6 +29,9 @@ void VExporter::prepareExport(const ExportOption &p_opt) p_opt.m_renderStyle, p_opt.m_renderCodeBlockStyle, p_opt.m_format == ExportFormat::PDF); + + m_exportHtmlTemplate = VUtils::generateExportHtmlTemplate(p_opt.m_renderBg); + m_pageLayout = *(p_opt.m_layout); } @@ -37,70 +40,15 @@ bool VExporter::exportPDF(VFile *p_file, const QString &p_outputFile, QString *p_errMsg) { - Q_UNUSED(p_errMsg); + return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg); +} - bool ret = false; - - bool isOpened = p_file->isOpened(); - if (!isOpened && !p_file->open()) { - goto exit; - } - - Q_ASSERT(m_state == ExportState::Idle); - m_state = ExportState::Busy; - - clearNoteState(); - - initWebViewer(p_file, p_opt); - - while (!isNoteStateReady()) { - VUtils::sleepWait(100); - - if (m_state == ExportState::Cancelled) { - goto exit; - } - - if (isNoteStateFailed()) { - m_state = ExportState::Failed; - goto exit; - } - } - - // Wait to ensure Web side is really ready. - VUtils::sleepWait(200); - - if (m_state == ExportState::Cancelled) { - goto exit; - } - - { - bool exportRet = exportToPDF(m_webViewer, - p_outputFile, - m_pageLayout); - - clearNoteState(); - - if (!isOpened) { - p_file->close(); - } - - if (exportRet) { - m_state = ExportState::Successful; - } else { - m_state = ExportState::Failed; - } - } - -exit: - clearWebViewer(); - - if (m_state == ExportState::Successful) { - ret = true; - } - - m_state = ExportState::Idle; - - return ret; +bool VExporter::exportHTML(VFile *p_file, + const ExportOption &p_opt, + const QString &p_outputFile, + QString *p_errMsg) +{ + return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg); } void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt) @@ -116,12 +64,12 @@ void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt) connect(page, &VPreviewPage::loadFinished, this, &VExporter::handleLoadFinished); - VDocument *document = new VDocument(p_file, m_webViewer); - connect(document, &VDocument::logicsFinished, + m_webDocument = new VDocument(p_file, m_webViewer); + connect(m_webDocument, &VDocument::logicsFinished, this, &VExporter::handleLogicsFinished); QWebChannel *channel = new QWebChannel(m_webViewer); - channel->registerObject(QStringLiteral("content"), document); + channel->registerObject(QStringLiteral("content"), m_webDocument); page->setWebChannel(channel); // Need to generate HTML using Hoedown. @@ -131,7 +79,7 @@ void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt) QString html = mdConverter.generateHtml(p_file->getContent(), g_config->getMarkdownExtensions(), toc); - document->setHtml(html); + m_webDocument->setHtml(html); } m_webViewer->setHtml(m_htmlTemplate, p_file->getBaseUrl()); @@ -155,10 +103,10 @@ void VExporter::handleLoadFinished(bool p_ok) void VExporter::clearWebViewer() { - if (m_webViewer) { - delete m_webViewer; - m_webViewer = NULL; - } + // m_webDocument will be freeed by QObject. + delete m_webViewer; + m_webViewer = NULL; + m_webDocument = NULL; } bool VExporter::exportToPDF(VWebView *p_webViewer, @@ -198,3 +146,135 @@ bool VExporter::exportToPDF(VWebView *p_webViewer, return pdfPrinted == 1; } +bool VExporter::exportViaWebView(VFile *p_file, + const ExportOption &p_opt, + const QString &p_outputFile, + QString *p_errMsg) +{ + Q_UNUSED(p_errMsg); + + bool ret = false; + + bool isOpened = p_file->isOpened(); + if (!isOpened && !p_file->open()) { + goto exit; + } + + Q_ASSERT(m_state == ExportState::Idle); + m_state = ExportState::Busy; + + clearNoteState(); + + initWebViewer(p_file, p_opt); + + while (!isNoteStateReady()) { + VUtils::sleepWait(100); + + if (m_state == ExportState::Cancelled) { + goto exit; + } + + if (isNoteStateFailed()) { + m_state = ExportState::Failed; + goto exit; + } + } + + // Wait to ensure Web side is really ready. + VUtils::sleepWait(200); + + if (m_state == ExportState::Cancelled) { + goto exit; + } + + { + + bool exportRet = false; + switch (p_opt.m_format) { + case ExportFormat::PDF: + exportRet = exportToPDF(m_webViewer, + p_outputFile, + m_pageLayout); + break; + + case ExportFormat::HTML: + exportRet = exportToHTML(m_webViewer, + m_webDocument, + p_outputFile); + break; + + default: + break; + } + + clearNoteState(); + + if (!isOpened) { + p_file->close(); + } + + if (exportRet) { + m_state = ExportState::Successful; + } else { + m_state = ExportState::Failed; + } + + } + +exit: + clearWebViewer(); + + if (m_state == ExportState::Successful) { + ret = true; + } + + m_state = ExportState::Idle; + + return ret; +} + +bool VExporter::exportToHTML(VWebView *p_webViewer, + VDocument *p_webDocument, + const QString &p_filePath) +{ + Q_UNUSED(p_webViewer); + int htmlExported = 0; + + connect(p_webDocument, &VDocument::htmlContentFinished, + this, [&, this](const QString &p_headContent, const QString &p_bodyContent) { + if (p_bodyContent.isEmpty() || this->m_state == ExportState::Cancelled) { + htmlExported = -1; + return; + } + + Q_ASSERT(!p_filePath.isEmpty()); + + QFile file(p_filePath); + + if (!file.open(QFile::WriteOnly)) { + htmlExported = -1; + return; + } + + QString html(m_exportHtmlTemplate); + html.replace(HtmlHolder::c_headHolder, p_headContent); + html.replace(HtmlHolder::c_bodyHolder, p_bodyContent); + + file.write(html.toUtf8()); + file.close(); + + htmlExported = 1; + }); + + p_webDocument->getHtmlContentAsync(); + + while (htmlExported == 0) { + VUtils::sleepWait(100); + + if (m_state == ExportState::Cancelled) { + break; + } + } + + return htmlExported == 1; +} diff --git a/src/vexporter.h b/src/vexporter.h index 0bf81b93..caca42db 100644 --- a/src/vexporter.h +++ b/src/vexporter.h @@ -8,6 +8,7 @@ class QWidget; class VWebView; +class VDocument; class VExporter : public QObject { @@ -21,6 +22,11 @@ public: const QString &p_outputFile, QString *p_errMsg = NULL); + bool exportHTML(VFile *p_file, + const ExportOption &p_opt, + const QString &p_outputFile, + QString *p_errMsg = NULL); + private slots: void handleLogicsFinished(); @@ -57,17 +63,31 @@ private: bool isNoteStateFailed() const; + bool exportViaWebView(VFile *p_file, + const ExportOption &p_opt, + const QString &p_outputFile, + QString *p_errMsg = NULL); + bool exportToPDF(VWebView *p_webViewer, const QString &p_filePath, const QPageLayout &p_layout); + bool exportToHTML(VWebView *p_webViewer, + VDocument *p_webDocument, + const QString &p_filePath); + QPageLayout m_pageLayout; // Will be allocated and free for each conversion. VWebView *m_webViewer; + VDocument *m_webDocument; + QString m_htmlTemplate; + // Template to hold the export HTML result. + QString m_exportHtmlTemplate; + NoteState m_noteState; ExportState m_state; diff --git a/src/vnote.cpp b/src/vnote.cpp index 270313fc..e4ff6996 100644 --- a/src/vnote.cpp +++ b/src/vnote.cpp @@ -110,7 +110,7 @@ QString VNote::generateHtmlTemplate(const QString &p_renderBg, cssStyle += "img { max-width: 100% !important; height: auto !important; }\n"; } - const QString styleHolder(""); + const QString styleHolder("/* BACKGROUND_PLACE_HOLDER */"); const QString cssHolder("CSS_PLACE_HOLDER"); const QString codeBlockCssHolder("HIGHLIGHTJS_CSS_PLACE_HOLDER"); @@ -142,6 +142,32 @@ QString VNote::generateHtmlTemplate(const QString &p_renderBg, return templ; } +QString VNote::generateExportHtmlTemplate(const QString &p_renderBg) +{ + const QString c_exportTemplatePath(":/resources/export_template.html"); + + QString cssStyle; + if (!p_renderBg.isEmpty()) { + cssStyle += "body { background-color: " + p_renderBg + " !important; }\n"; + } + + if (g_config->getEnableImageConstraint()) { + // Constain the image width. + cssStyle += "img { max-width: 100% !important; height: auto !important; }\n"; + } + + const QString styleHolder("/* BACKGROUND_PLACE_HOLDER */"); + + QString templ = VUtils::readFileFromDisk(c_exportTemplatePath); + g_palette->fillStyle(templ); + + if (!cssStyle.isEmpty()) { + templ.replace(styleHolder, cssStyle); + } + + return templ; +} + void VNote::updateTemplate() { QString renderBg = g_config->getRenderBackgroundColor(g_config->getCurRenderBackgroundColor()); diff --git a/src/vnote.h b/src/vnote.h index 7a10dbce..46a68a15 100644 --- a/src/vnote.h +++ b/src/vnote.h @@ -105,6 +105,9 @@ public: const QString &p_codeBlockStyleUrl, bool p_isPDF); + // @p_renderBg: background color, empty to not specify given color. + static QString generateExportHtmlTemplate(const QString &p_renderBg); + public slots: void updateTemplate(); diff --git a/src/vnote.qrc b/src/vnote.qrc index 6004799c..064f3459 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -239,5 +239,6 @@ resources/icons/delete_cart_item.svg resources/icons/fullscreen.svg resources/icons/menubar.svg + resources/export_template.html