Browse Source

export: support HTML format

Le Tan 7 years ago
parent
commit
1fe7567d79

+ 40 - 1
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) {

+ 5 - 0
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();
 

+ 14 - 0
src/resources/export_template.html

@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="en">
+<meta charset="utf-8">
+<head>
+    <style type="text/css">
+    /* BACKGROUND_PLACE_HOLDER */
+    </style>
+
+<!-- HEAD_PLACE_HOLDER -->
+</head>
+<body>
+<!-- BODY_PLACE_HOLDER -->
+</body>
+</html>

+ 2 - 2
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);
-}
+};

+ 1 - 1
src/resources/markdown_template.html

@@ -3,7 +3,7 @@
 <meta charset="utf-8">
 <head>
     <style type="text/css">
-        <!-- BACKGROUND_PLACE_HOLDER -->
+    /* BACKGROUND_PLACE_HOLDER */
     </style>
 
     <style type="text/css">

+ 28 - 4
src/resources/markdown_template.js

@@ -40,6 +40,26 @@ if (typeof VEnableImageCaption == 'undefined') {
     VEnableImageCaption = false;
 }
 
+var headContent = function() {
+    var styles = "<style type=\"text/css\">\n";
+
+    for (var i = 0; i < document.styleSheets.length; ++i) {
+        var styleSheet = document.styleSheets[i];
+        if (styleSheet.cssRules) {
+            for (var j = 0; j < styleSheet.cssRules.length; ++j) {
+                styles = styles + styleSheet.cssRules[j].cssText + "\n";
+            }
+        }
+    }
+
+    var styles = styles + "</style>";
+    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);
     }
 }

+ 1 - 1
src/resources/simple_template.html

@@ -6,6 +6,6 @@
     <link rel="stylesheet" type="text/css" href="qrc:/resources/typewriter.css">
 </head>
 <body>
-    <!-- BODY_PLACE_HOLDER -->
+<!-- BODY_PLACE_HOLDER -->
 </body>
 </html>

+ 1 - 1
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

+ 5 - 0
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)

+ 3 - 0
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.

+ 1 - 0
src/vconstants.h

@@ -37,6 +37,7 @@ namespace HtmlHolder
     static const QString c_JSHolder = "JS_PLACE_HOLDER";
     static const QString c_extraHolder = "<!-- EXTRA_PLACE_HOLDER -->";
     static const QString c_bodyHolder = "<!-- BODY_PLACE_HOLDER -->";
+    static const QString c_headHolder = "<!-- HEAD_PLACE_HOLDER -->";
 }
 
 // Directory Config file items.

+ 10 - 0
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);
+}

+ 10 - 0
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;

+ 151 - 71
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);
-
-    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 exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg);
+}
 
-    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;
+}

+ 20 - 0
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;

+ 27 - 1
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("<!-- BACKGROUND_PLACE_HOLDER -->");
+    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());

+ 3 - 0
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();
 

+ 1 - 0
src/vnote.qrc

@@ -239,5 +239,6 @@
         <file>resources/icons/delete_cart_item.svg</file>
         <file>resources/icons/fullscreen.svg</file>
         <file>resources/icons/menubar.svg</file>
+        <file>resources/export_template.html</file>
     </qresource>
 </RCC>