1
0
Le Tan 6 жил өмнө
parent
commit
f72d6e6299

+ 1505 - 0
src/dialog/vexportdialog.cpp

@@ -0,0 +1,1505 @@
+#include "vexportdialog.h"
+
+#include <QtWidgets>
+#include <QCoreApplication>
+#include <QProcess>
+#include <QTemporaryDir>
+#include <QMimeData>
+#include <QApplication>
+#include <QClipboard>
+
+#ifndef QT_NO_PRINTER
+#include <QPrinter>
+#include <QPageSetupDialog>
+#endif
+
+#include "utils/vutils.h"
+#include "utils/vclipboardutils.h"
+#include "vlineedit.h"
+#include "vnotebook.h"
+#include "vfile.h"
+#include "vdirectory.h"
+#include "vcart.h"
+#include "vconfigmanager.h"
+#include "vnotefile.h"
+#include "vnote.h"
+#include "vexporter.h"
+#include "vlineedit.h"
+#include "utils/vprocessutils.h"
+
+extern VConfigManager *g_config;
+
+extern VNote *g_vnote;
+
+QString VExportDialog::s_lastOutputFolder;
+
+ExportOption VExportDialog::s_opt;
+
+#define LOGERR(x) do { QString msg = (x); \
+                       VUtils::addErrMsg(p_errMsg, msg); \
+                       appendLogLine(msg); \
+                  } while (0)
+
+VExportDialog::VExportDialog(VNotebook *p_notebook,
+                             VDirectory *p_directory,
+                             VFile *p_file,
+                             VCart *p_cart,
+                             MarkdownConverterType p_renderer,
+                             QWidget *p_parent)
+    : QDialog(p_parent),
+      m_notebook(p_notebook),
+      m_directory(p_directory),
+      m_file(p_file),
+      m_cart(p_cart),
+      m_pageLayout(QPageLayout(QPageSize(QPageSize::A4),
+                               QPageLayout::Portrait,
+                               QMarginsF(10, 16, 10, 10),
+                               QPageLayout::Millimeter)),
+      m_inExport(false),
+      m_askedToStop(false)
+{
+    if (s_lastOutputFolder.isEmpty()) {
+        s_lastOutputFolder = g_config->getExportFolderPath();
+    }
+
+    setupUI();
+
+    m_exporter = new VExporter(this);
+    connect(m_exporter, &VExporter::outputLog,
+            this, [this](const QString &p_log) {
+                appendLogLine(p_log);
+            });
+
+    initUIFields(p_renderer);
+
+    handleInputChanged();
+}
+
+void VExportDialog::setupUI()
+{
+    // Notes to export.
+    m_srcCB = VUtils::getComboBox();
+    m_srcCB->setToolTip(tr("Choose notes to export"));
+    m_srcCB->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+    connect(m_srcCB, SIGNAL(currentIndexChanged(int)),
+            this, SLOT(handleCurrentSrcChanged(int)));
+
+    // Target format.
+    m_formatCB = VUtils::getComboBox();
+    m_formatCB->setToolTip(tr("Choose target format to export as"));
+    m_formatCB->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+    connect(m_formatCB, SIGNAL(currentIndexChanged(int)),
+            this, SLOT(handleCurrentFormatChanged(int)));
+
+    // Markdown renderer.
+    m_rendererCB = VUtils::getComboBox();
+    m_rendererCB->setToolTip(tr("Choose converter to render Markdown"));
+    m_rendererCB->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+
+    // Markdown rendering background.
+    m_renderBgCB = VUtils::getComboBox();
+    m_renderBgCB->setToolTip(tr("Choose rendering background color for Markdown"));
+
+    // Markdown rendering style.
+    m_renderStyleCB = VUtils::getComboBox();
+    m_renderStyleCB->setToolTip(tr("Choose rendering style for Markdown"));
+
+    // Markdown rendering code block style.
+    m_renderCodeBlockStyleCB = VUtils::getComboBox();
+    m_renderCodeBlockStyleCB->setToolTip(tr("Choose rendering code block style for Markdown"));
+
+    // Output directory.
+    m_outputEdit = new VLineEdit(s_lastOutputFolder);
+    connect(m_outputEdit, &QLineEdit::textChanged,
+            this, &VExportDialog::handleInputChanged);
+    QPushButton *browseBtn = new QPushButton(tr("&Browse"));
+    connect(browseBtn, &QPushButton::clicked,
+            this, &VExportDialog::handleOutputBrowseBtnClicked);
+    QHBoxLayout *outputLayout = new QHBoxLayout();
+    outputLayout->addWidget(m_outputEdit);
+    outputLayout->addWidget(browseBtn);
+
+    m_basicBox = new QGroupBox(tr("Information"));
+
+    m_settingBox = new QGroupBox(tr("Advanced Settings"));
+
+    m_consoleEdit = new QPlainTextEdit();
+    m_consoleEdit->setReadOnly(true);
+    m_consoleEdit->setLineWrapMode(QPlainTextEdit::WidgetWidth);
+    m_consoleEdit->setProperty("LineEdit", true);
+    m_consoleEdit->setPlaceholderText(tr("Output logs will be shown here"));
+
+    // Ok is the default button.
+    m_btnBox = new QDialogButtonBox(QDialogButtonBox::Close);
+    m_exportBtn = m_btnBox->addButton(tr("Export"), QDialogButtonBox::ActionRole);
+    m_openBtn = m_btnBox->addButton(tr("Open Directory"), QDialogButtonBox::ActionRole);
+    m_openBtn->setToolTip(tr("Open output directory"));
+    m_copyBtn = m_btnBox->addButton(tr("Copy Content"), QDialogButtonBox::ActionRole);
+    m_copyBtn->setToolTip(tr("Copy the content of the exported file"));
+    m_copyBtn->setEnabled(false);
+    connect(m_btnBox, &QDialogButtonBox::rejected,
+            this, [this]() {
+                if (m_inExport) {
+                    // Just cancel the export. Do not exit.
+                    m_askedToStop = true;
+                    m_exporter->setAskedToStop(true);
+                    appendLogLine(tr("Cancelling the export..."));
+                } else {
+                    QDialog::reject();
+                }
+            });
+
+    m_exportBtn->setProperty("SpecialBtn", true);
+    connect(m_exportBtn, &QPushButton::clicked,
+            this, &VExportDialog::startExport);
+
+    connect(m_openBtn, &QPushButton::clicked,
+            this, [this]() {
+                QUrl url = QUrl::fromLocalFile(getOutputDirectory());
+                QDesktopServices::openUrl(url);
+            });
+
+    connect(m_copyBtn, &QPushButton::clicked,
+            this, [this]() {
+                if (m_exportedFile.isEmpty()) {
+                    return;
+                }
+
+                bool ret = false;
+                QString content = VUtils::readFileFromDisk(m_exportedFile);
+                if (!content.isNull()) {
+                    QMimeData *data = new QMimeData();
+                    data->setText(content);
+                    VClipboardUtils::setMimeDataToClipboard(QApplication::clipboard(), data, QClipboard::Clipboard);
+                    ret = true;
+                }
+
+                if (ret) {
+                    appendLogLine(tr("Copied content of file %1").arg(m_exportedFile));
+                } else {
+                    appendLogLine(tr("Fail to copy content of file %1").arg(m_exportedFile));
+                }
+            });
+
+    // Progress bar.
+    m_proBar = new QProgressBar();
+    m_proBar->setRange(0, 0);
+    m_proBar->hide();
+
+    QHBoxLayout *btnLayout = new QHBoxLayout();
+    btnLayout->addWidget(m_proBar);
+    btnLayout->addWidget(m_btnBox);
+
+    QFormLayout *basicLayout = new QFormLayout();
+    basicLayout->addRow(tr("Notes to export:"), m_srcCB);
+    basicLayout->addRow(tr("Target format:"), m_formatCB);
+    basicLayout->addRow(tr("Markdown renderer:"), m_rendererCB);
+    basicLayout->addRow(tr("Markdown rendering background:"), m_renderBgCB);
+    basicLayout->addRow(tr("Markdown rendering style:"), m_renderStyleCB);
+    basicLayout->addRow(tr("Markdown rendering code block style:"), m_renderCodeBlockStyleCB);
+    basicLayout->addRow(tr("Output directory:"), outputLayout);
+
+    m_basicBox->setLayout(basicLayout);
+
+    // Settings box.
+    m_generalSettings = setupGeneralAdvancedSettings();
+    m_htmlSettings = setupHTMLAdvancedSettings();
+    m_pdfSettings = setupPDFAdvancedSettings();
+    m_customSettings = setupCustomAdvancedSettings();
+
+    QVBoxLayout *advLayout = new QVBoxLayout();
+    advLayout->addWidget(m_generalSettings);
+    advLayout->addWidget(m_htmlSettings);
+    advLayout->addWidget(m_pdfSettings);
+    advLayout->addWidget(m_customSettings);
+
+    m_settingBox->setLayout(advLayout);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addWidget(m_basicBox);
+    mainLayout->addWidget(m_settingBox);
+    mainLayout->addWidget(m_consoleEdit);
+    mainLayout->addLayout(btnLayout);
+
+    setLayout(mainLayout);
+
+    setWindowTitle(tr("Export"));
+}
+
+QWidget *VExportDialog::setupPDFAdvancedSettings()
+{
+    // Page layout settings.
+    m_layoutLabel = new QLabel();
+    QPushButton *layoutBtn = new QPushButton(tr("Settings"));
+
+#ifndef QT_NO_PRINTER
+    connect(layoutBtn, &QPushButton::clicked,
+            this, &VExportDialog::handleLayoutBtnClicked);
+#else
+    layoutBtn->hide();
+#endif
+
+    updatePageLayoutLabel();
+
+    // Enable table of contents.
+    m_tableOfContentsCB = new QCheckBox(tr("Enable Table Of Contents"));
+    m_tableOfContentsCB->setToolTip(tr("Add a table of contents to the document"));
+
+    // Use wkhtmltopdf.
+    m_wkhtmltopdfCB = new QCheckBox(tr("Use wkhtmltopdf"));
+    m_wkhtmltopdfCB->setToolTip(tr("Use wkhtmltopdf tool to generate PDF (wkhtmltopdf needed to be installed)"));
+    connect(m_wkhtmltopdfCB, &QCheckBox::stateChanged,
+            this, [this](int p_state) {
+                bool checked = p_state == Qt::Checked;
+                m_wkPathEdit->setEnabled(checked);
+                m_wkPathBrowseBtn->setEnabled(checked);
+                m_wkBackgroundCB->setEnabled(checked);
+                m_wkPageNumberCB->setEnabled(checked);
+                m_wkExtraArgsEdit->setEnabled(checked);
+            });
+
+    QPushButton *wkBtn = new QPushButton(tr("Download wkhtmltopdf"));
+    connect(wkBtn, &QPushButton::clicked,
+            this, [this]() {
+                QString url("https://wkhtmltopdf.org/downloads.html");
+                QDesktopServices::openUrl(QUrl(url));
+            });
+
+    // wkhtmltopdf Path.
+    m_wkPathEdit = new VLineEdit();
+    m_wkPathEdit->setToolTip(tr("Tell VNote where to find wkhtmltopdf tool"));
+    m_wkPathEdit->setEnabled(false);
+
+    m_wkPathBrowseBtn = new QPushButton(tr("&Browse"));
+    connect(m_wkPathBrowseBtn, &QPushButton::clicked,
+            this, &VExportDialog::handleWkPathBrowseBtnClicked);
+    m_wkPathBrowseBtn->setEnabled(false);
+
+    m_wkTitleEdit = new VLineEdit();
+    m_wkTitleEdit->setPlaceholderText(tr("Empty to use the name of the first source file"));
+    m_wkTitleEdit->setToolTip(tr("Title of the generated PDF file"));
+    m_wkTitleEdit->setEnabled(false);
+
+    m_wkTargetFileNameEdit = new VLineEdit();
+    m_wkTargetFileNameEdit->setPlaceholderText(tr("Empty to use the name of the first source file"));
+    m_wkTargetFileNameEdit->setToolTip(tr("Name of the generated PDF file"));
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_wkTargetFileNameEdit);
+    m_wkTargetFileNameEdit->setValidator(validator);
+    m_wkTargetFileNameEdit->setEnabled(false);
+
+    // wkhtmltopdf enable background.
+    m_wkBackgroundCB = new QCheckBox(tr("Enable background"));
+    m_wkBackgroundCB->setToolTip(tr("Enable background when printing"));
+    m_wkBackgroundCB->setEnabled(false);
+
+    // wkhtmltopdf page number.
+    m_wkPageNumberCB = VUtils::getComboBox();
+    m_wkPageNumberCB->setToolTip(tr("Append page number as footer"));
+    m_wkPageNumberCB->setEnabled(false);
+
+    // wkhtmltopdf extra argumnets.
+    m_wkExtraArgsEdit = new VLineEdit();
+    m_wkExtraArgsEdit->setToolTip(tr("Additional global options passed to wkhtmltopdf"));
+    m_wkExtraArgsEdit->setPlaceholderText(tr("Use \" to enclose options containing spaces"));
+    m_wkExtraArgsEdit->setEnabled(false);
+
+    QGridLayout *advLayout = new QGridLayout();
+    advLayout->addWidget(new QLabel(tr("Page layout:")), 0, 0);
+    advLayout->addWidget(m_layoutLabel, 0, 1);
+    advLayout->addWidget(layoutBtn, 0, 2);
+    advLayout->addWidget(m_tableOfContentsCB, 0, 4, 1, 2);
+
+    advLayout->addWidget(m_wkhtmltopdfCB, 1, 1, 1, 2);
+    advLayout->addWidget(wkBtn, 1, 4, 1, 2);
+
+    advLayout->addWidget(new QLabel(tr("wkhtmltopdf path:")), 2, 0);
+    advLayout->addWidget(m_wkPathEdit, 2, 1, 1, 4);
+    advLayout->addWidget(m_wkPathBrowseBtn, 2, 5);
+
+    advLayout->addWidget(new QLabel(tr("Title:")), 3, 0);
+    advLayout->addWidget(m_wkTitleEdit, 3, 1, 1, 2);
+
+    advLayout->addWidget(new QLabel(tr("Output file name:")), 3, 3);
+    advLayout->addWidget(m_wkTargetFileNameEdit, 3, 4, 1, 2);
+
+    advLayout->addWidget(new QLabel(tr("Page number:")), 4, 0);
+    advLayout->addWidget(m_wkPageNumberCB, 4, 1, 1, 2);
+    advLayout->addWidget(m_wkBackgroundCB, 4, 4, 1, 2);
+
+    advLayout->addWidget(new QLabel(tr("Additional options:")), 5, 0);
+    advLayout->addWidget(m_wkExtraArgsEdit, 5, 1, 1, 5);
+
+    advLayout->setContentsMargins(0, 0, 0, 0);
+
+    QWidget *wid = new QWidget();
+    wid->setLayout(advLayout);
+
+    return wid;
+}
+
+QWidget *VExportDialog::setupHTMLAdvancedSettings()
+{
+    // Embed CSS styles.
+    m_embedStyleCB = new QCheckBox(tr("Embed CSS styles"), this);
+    m_embedStyleCB->setToolTip(tr("Embed CSS styles in HTML file"));
+
+    // Embed images as data URI.
+    m_embedImagesCB = new QCheckBox(tr("Embed images"), this);
+    m_embedImagesCB->setToolTip(tr("Embed images as data URI"));
+
+    // 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"));
+    connect(m_completeHTMLCB, &QCheckBox::stateChanged,
+            this, [this](int p_state) {
+                bool checked = p_state == Qt::Checked;
+                m_embedImagesCB->setEnabled(checked);
+                if (!checked) {
+                    m_embedImagesCB->setChecked(false);
+                }
+            });
+
+    // Mime HTML.
+    m_mimeHTMLCB = new QCheckBox(tr("MIME HTML"), this);
+    m_mimeHTMLCB->setToolTip(tr("Export as a complete web page in MIME HTML format"));
+    connect(m_mimeHTMLCB, &QCheckBox::stateChanged,
+            this, [this](int p_state) {
+                bool checked = p_state == Qt::Checked;
+                m_embedStyleCB->setEnabled(!checked);
+                m_completeHTMLCB->setEnabled(!checked);
+            });
+
+    // Outline panel.
+    m_outlinePanelCB = new QCheckBox(tr("Enable outline panel"), this);
+    m_outlinePanelCB->setToolTip(tr("Add an outline panel in HTML file"));
+
+    QGridLayout *advLayout = new QGridLayout();
+    advLayout->addWidget(m_embedStyleCB, 0, 1, 1, 2);
+    advLayout->addWidget(m_completeHTMLCB, 0, 4, 1, 2);
+    advLayout->addWidget(m_embedImagesCB, 1, 1, 1, 2);
+    advLayout->addWidget(m_mimeHTMLCB, 1, 4, 1, 2);
+    advLayout->addWidget(m_outlinePanelCB, 2, 1, 1, 2);
+
+    advLayout->setContentsMargins(0, 0, 0, 0);
+
+    QWidget *wid = new QWidget();
+    wid->setLayout(advLayout);
+
+    return wid;
+}
+
+QWidget *VExportDialog::setupGeneralAdvancedSettings()
+{
+    // Include subfolders.
+    m_subfolderCB = new QCheckBox(tr("Process subfolders"));
+    m_subfolderCB->setToolTip(tr("Process subfolders recursively"));
+
+    QFormLayout *advLayout = new QFormLayout();
+    advLayout->addRow(m_subfolderCB);
+
+    advLayout->setContentsMargins(0, 0, 0, 0);
+
+    QWidget *wid = new QWidget();
+    wid->setLayout(advLayout);
+
+    return wid;
+}
+
+void VExportDialog::initUIFields(MarkdownConverterType p_renderer)
+{
+    // Notes to export.
+    if (m_file) {
+        m_srcCB->addItem(tr("Current Note (%1)").arg(m_file->getName()),
+                         (int)ExportSource::CurrentNote);
+    }
+
+    if (m_directory) {
+        m_srcCB->addItem(tr("Current Folder (%1)").arg(m_directory->getName()),
+                         (int)ExportSource::CurrentFolder);
+    }
+
+    if (m_notebook) {
+        m_srcCB->addItem(tr("Current Notebook (%1)").arg(m_notebook->getName()),
+                         (int)ExportSource::CurrentNotebook);
+    }
+
+    if (m_cart && m_cart->count() > 0) {
+        m_srcCB->addItem(tr("Cart (%1)").arg(m_cart->count()),
+                         (int)ExportSource::Cart);
+    }
+
+    m_subfolderCB->setChecked(s_opt.m_processSubfolders);
+
+    // Export format.
+    m_formatCB->addItem(tr("Markdown"), (int)ExportFormat::Markdown);
+    m_formatCB->addItem(tr("HTML"), (int)ExportFormat::HTML);
+    m_formatCB->addItem(tr("PDF"), (int)ExportFormat::PDF);
+    m_formatCB->addItem(tr("PDF (All In One)"), (int)ExportFormat::OnePDF);
+    m_formatCB->addItem(tr("Custom"), (int)ExportFormat::Custom);
+    m_formatCB->setCurrentIndex(m_formatCB->findData((int)s_opt.m_format));
+
+    // Markdown renderer.
+    m_rendererCB->addItem(tr("Hoedown"), MarkdownConverterType::Hoedown);
+    m_rendererCB->addItem(tr("Marked"), MarkdownConverterType::Marked);
+    m_rendererCB->addItem(tr("Markdown-it"), MarkdownConverterType::MarkdownIt);
+    m_rendererCB->addItem(tr("Showdown"), MarkdownConverterType::Showdown);
+    m_rendererCB->setCurrentIndex(m_rendererCB->findData(p_renderer));
+
+    // Markdown rendering background.
+    m_renderBgCB->addItem(tr("System"), "System");
+    m_renderBgCB->addItem(tr("Transparent"), "Transparent");
+    const QVector<VColor> &bgColors = g_config->getCustomColors();
+    for (int i = 0; i < bgColors.size(); ++i) {
+        m_renderBgCB->addItem(bgColors[i].m_name, bgColors[i].m_name);
+    }
+
+    if (s_opt.m_renderBg.isEmpty()) {
+        s_opt.m_renderBg = g_config->getCurRenderBackgroundColor();
+    }
+
+    m_renderBgCB->setCurrentIndex(m_renderBgCB->findData(s_opt.m_renderBg));
+
+    // Markdown rendering style.
+    QList<QString> styles = g_config->getCssStyles();
+    for (auto const &style : styles) {
+        m_renderStyleCB->addItem(style, style);
+    }
+
+    m_renderStyleCB->setCurrentIndex(
+        m_renderStyleCB->findData(g_config->getCssStyle()));
+
+    // Markdown rendering code block style.
+    QList<QString> cbStyles = g_config->getCodeBlockCssStyles();
+    for (auto const &style : cbStyles) {
+        m_renderCodeBlockStyleCB->addItem(style, style);
+    }
+
+    m_renderCodeBlockStyleCB->setCurrentIndex(
+        m_renderCodeBlockStyleCB->findData(g_config->getCodeBlockCssStyle()));
+
+    m_embedStyleCB->setChecked(s_opt.m_htmlOpt.m_embedCssStyle);
+
+    m_completeHTMLCB->setChecked(s_opt.m_htmlOpt.m_completeHTML);
+
+    m_embedImagesCB->setChecked(s_opt.m_htmlOpt.m_embedImages);
+
+    // m_mimeHTMLCB->setChecked(s_opt.m_htmlOpt.m_mimeHTML);
+    m_mimeHTMLCB->setChecked(false);
+    m_mimeHTMLCB->setEnabled(false);
+
+    m_outlinePanelCB->setChecked(s_opt.m_htmlOpt.m_outlinePanel);
+
+    m_tableOfContentsCB->setChecked(s_opt.m_pdfOpt.m_enableTableOfContents);
+
+    // m_wkhtmltopdfCB->setChecked(s_opt.m_pdfOpt.m_wkhtmltopdf);
+    m_wkhtmltopdfCB->setChecked(true);
+    m_wkhtmltopdfCB->setEnabled(false);
+
+    // wkhtmltopdf path.
+    m_wkPathEdit->setText(g_config->getWkhtmltopdfPath());
+
+    m_wkBackgroundCB->setChecked(s_opt.m_pdfOpt.m_wkEnableBackground);
+
+    // wkhtmltopdf page number.
+    m_wkPageNumberCB->addItem(tr("None"), (int)ExportPageNumber::None);
+    m_wkPageNumberCB->addItem(tr("Left"), (int)ExportPageNumber::Left);
+    m_wkPageNumberCB->addItem(tr("Center"), (int)ExportPageNumber::Center);
+    m_wkPageNumberCB->addItem(tr("Right"), (int)ExportPageNumber::Right);
+    m_wkPageNumberCB->setCurrentIndex(m_wkPageNumberCB->findData((int)s_opt.m_pdfOpt.m_wkPageNumber));
+
+    m_wkExtraArgsEdit->setText(g_config->getWkhtmltopdfArgs());
+
+    // Custom export.
+    // Read from config every time.
+    ExportCustomOption customOpt(g_config->getCustomExport());
+
+    m_customSrcFormatCB->addItem(tr("Markdown"), (int)ExportCustomOption::SourceFormat::Markdown);
+    m_customSrcFormatCB->addItem(tr("HTML"), (int)ExportCustomOption::SourceFormat::HTML);
+    m_customSrcFormatCB->setCurrentIndex(m_customSrcFormatCB->findData((int)customOpt.m_srcFormat));
+
+    m_customSuffixEdit->setText(customOpt.m_outputSuffix);
+
+    m_customCmdEdit->setPlainText(customOpt.m_cmd);
+
+    m_customAllInOneCB->setChecked(s_opt.m_customOpt.m_allInOne);
+
+    m_customPdfLikeCB->setChecked(s_opt.m_customOpt.m_pdfLike);
+
+    m_customFolderSepEdit->setText(s_opt.m_customOpt.m_folderSep);
+}
+
+bool VExportDialog::checkWkhtmltopdfExecutable(const QString &p_file)
+{
+    QStringList args;
+    args << "--version";
+    QByteArray out, err;
+    int exitCode = -1;
+    int ret = VProcessUtils::startProcess(p_file, args, exitCode, out, err);
+    switch (ret) {
+    case -2:
+        appendLogLine(tr("Fail to start wkhtmltopdf (%1).").arg(p_file));
+        break;
+
+    case -1:
+        appendLogLine(tr("wkhtmltopdf crashed (%1).").arg(p_file));
+        break;
+
+    case 0:
+        appendLogLine(tr("Use %1 (%2).").arg(p_file).arg(exitCode));
+        break;
+
+    default:
+        Q_ASSERT(false);
+        break;
+    }
+
+    return ret == 0;
+}
+
+void VExportDialog::startExport()
+{
+    if (m_inExport) {
+        return;
+    }
+
+    m_exportBtn->setEnabled(false);
+    m_proBar->show();
+    m_askedToStop = false;
+    m_exporter->setAskedToStop(false);
+    m_inExport = true;
+    m_exportedFile.clear();
+    m_copyBtn->setEnabled(false);
+
+    QString outputFolder = QDir::cleanPath(QDir(getOutputDirectory()).absolutePath());
+
+    QString renderStyle = m_renderStyleCB->currentData().toString();
+    QString renderCodeBlockStyle = m_renderCodeBlockStyleCB->currentData().toString();
+
+    s_opt = ExportOption(currentSource(),
+                         currentFormat(),
+                         (MarkdownConverterType)m_rendererCB->currentData().toInt(),
+                         m_renderBgCB->currentData().toString(),
+                         renderStyle,
+                         renderCodeBlockStyle,
+                         m_subfolderCB->isChecked(),
+                         ExportPDFOption(&m_pageLayout,
+                                         // m_wkhtmltopdfCB->isChecked(),
+                                         true,
+                                         QDir::toNativeSeparators(m_wkPathEdit->text()),
+                                         m_wkBackgroundCB->isChecked(),
+                                         m_tableOfContentsCB->isChecked(),
+                                         m_wkTitleEdit->text(),
+                                         m_wkTargetFileNameEdit->text(),
+                                         (ExportPageNumber)
+                                         m_wkPageNumberCB->currentData().toInt(),
+                                         m_wkExtraArgsEdit->text()),
+                         ExportHTMLOption(m_embedStyleCB->isChecked(),
+                                          m_completeHTMLCB->isChecked(),
+                                          m_embedImagesCB->isChecked(),
+                                          m_mimeHTMLCB->isChecked(),
+                                          m_outlinePanelCB->isChecked()),
+                         ExportCustomOption((ExportCustomOption::SourceFormat)
+                                            m_customSrcFormatCB->currentData().toInt(),
+                                            m_customSuffixEdit->text(),
+                                            m_customCmdEdit->toPlainText(),
+                                            g_config->getCssStyleUrl(renderStyle),
+                                            g_config->getCodeBlockCssStyleUrl(renderCodeBlockStyle),
+                                            m_customAllInOneCB->isChecked(),
+                                            m_customPdfLikeCB->isChecked(),
+                                            m_customFolderSepEdit->text(),
+                                            m_customTargetFileNameEdit->text()));
+
+    m_consoleEdit->clear();
+    appendLogLine(tr("Export to %1.").arg(outputFolder));
+
+    if (s_opt.m_format == ExportFormat::PDF
+        || s_opt.m_format == ExportFormat::OnePDF
+        || s_opt.m_format == ExportFormat::HTML) {
+        if (s_opt.m_format != ExportFormat::OnePDF) {
+            s_opt.m_pdfOpt.m_wkTitle.clear();
+        }
+
+        if ((s_opt.m_format == ExportFormat::PDF
+             && s_opt.m_pdfOpt.m_wkhtmltopdf)
+            || s_opt.m_format == ExportFormat::OnePDF) {
+            g_config->setWkhtmltopdfPath(s_opt.m_pdfOpt.m_wkPath);
+            g_config->setWkhtmltopdfArgs(s_opt.m_pdfOpt.m_wkExtraArgs);
+
+            if (!checkWkhtmltopdfExecutable(s_opt.m_pdfOpt.m_wkPath)) {
+                m_inExport = false;
+                m_exportBtn->setEnabled(true);
+                m_proBar->hide();
+                return;
+            }
+        }
+
+        m_exporter->prepareExport(s_opt);
+    } else if (s_opt.m_format == ExportFormat::Custom) {
+        const ExportCustomOption &opt = s_opt.m_customOpt;
+        if (opt.m_srcFormat == ExportCustomOption::HTML) {
+            m_exporter->prepareExport(s_opt);
+        }
+
+        // Save it to config.
+        g_config->setCustomExport(opt.toConfig());
+
+        if (opt.m_outputSuffix.isEmpty()
+            || opt.m_cmd.isEmpty()
+            || (opt.m_allInOne && opt.m_folderSep.isEmpty())) {
+            appendLogLine(tr("Invalid configurations for custom export."));
+            m_inExport = false;
+            m_exportBtn->setEnabled(true);
+            m_proBar->hide();
+            return;
+        }
+    }
+
+    int ret = 0;
+    QString msg;
+
+    if (s_opt.m_format == ExportFormat::OnePDF) {
+        QList<QString> files;
+        // Output HTMLs to a tmp folder.
+        QTemporaryDir tmpDir;
+        if (!tmpDir.isValid()) {
+            goto exit;
+        }
+
+        ret = outputAsHTML(tmpDir.path(), &msg, &files);
+        if (m_askedToStop) {
+            ret = 0;
+            goto exit;
+        }
+
+        Q_ASSERT(ret == files.size());
+        if (!files.isEmpty()) {
+            ret = doExportPDFAllInOne(files, s_opt, outputFolder, &msg);
+        }
+    } else if (s_opt.m_format == ExportFormat::Custom
+               && s_opt.m_customOpt.m_allInOne) {
+        QList<QString> files;
+        QTemporaryDir tmpDir;
+        if (!tmpDir.isValid()) {
+            goto exit;
+        }
+
+        if (s_opt.m_customOpt.m_srcFormat == ExportCustomOption::HTML) {
+            // Output HTMLs to a tmp folder.
+            ret = outputAsHTML(tmpDir.path(), &msg, &files);
+            if (m_askedToStop) {
+                ret = 0;
+                goto exit;
+            }
+
+            Q_ASSERT(ret == files.size());
+        } else {
+            // Collect all markdown files.
+            files = collectFiles(&msg);
+        }
+
+        if (!files.isEmpty()) {
+            ret = doExportCustomAllInOne(files, s_opt, outputFolder, &msg);
+        }
+    } else {
+        switch (s_opt.m_source) {
+        case ExportSource::CurrentNote:
+        {
+            QStringList files;
+            ret = doExport(m_file, s_opt, outputFolder, &msg, &files);
+            if (ret == 1 && s_opt.m_format == ExportFormat::HTML) {
+                Q_ASSERT(files.size() == 1);
+                m_exportedFile = files.first();
+            }
+
+            break;
+        }
+
+        case ExportSource::CurrentFolder:
+            ret = doExport(m_directory, s_opt, outputFolder, &msg);
+            break;
+
+        case ExportSource::CurrentNotebook:
+            ret = doExport(m_notebook, s_opt, outputFolder, &msg);
+            break;
+
+        case ExportSource::Cart:
+            ret = doExport(m_cart, s_opt, outputFolder, &msg);
+            break;
+
+        default:
+            break;
+        }
+    }
+
+exit:
+
+    if (m_askedToStop) {
+        appendLogLine(tr("User cancelled the export. Aborted!"));
+        m_askedToStop = false;
+        m_exporter->setAskedToStop(false);
+    }
+
+    if (!msg.isEmpty()) {
+        VUtils::showMessage(QMessageBox::Warning,
+                            tr("Warning"),
+                            tr("Errors found during export."),
+                            msg,
+                            QMessageBox::Ok,
+                            QMessageBox::Ok,
+                            this);
+    }
+
+    appendLogLine(tr("%1 notes exported.").arg(ret));
+
+    m_inExport = false;
+    m_exportBtn->setEnabled(true);
+    m_proBar->hide();
+
+    m_copyBtn->setEnabled(!m_exportedFile.isEmpty());
+}
+
+QString VExportDialog::getOutputDirectory() const
+{
+    return m_outputEdit->text();
+}
+
+void VExportDialog::handleOutputBrowseBtnClicked()
+{
+    QString initPath = getOutputDirectory();
+    if (!QFileInfo::exists(initPath)) {
+        initPath = g_config->getDocumentPathOrHomePath();
+    }
+
+    QString dirPath = QFileDialog::getExistingDirectory(this,
+                                                        tr("Select Output Directory To Export To"),
+                                                        initPath,
+                                                        QFileDialog::ShowDirsOnly
+                                                        | QFileDialog::DontResolveSymlinks);
+
+    if (!dirPath.isEmpty()) {
+        m_outputEdit->setText(dirPath);
+        s_lastOutputFolder = dirPath;
+    }
+}
+
+void VExportDialog::handleWkPathBrowseBtnClicked()
+{
+    QString initPath = m_wkPathEdit->text();
+    if (!QFileInfo::exists(initPath)) {
+        initPath = QCoreApplication::applicationDirPath();
+    }
+
+#if defined(Q_OS_WIN)
+    QString filter = tr("Executable (*.exe)");
+#else
+    QString filter;
+#endif
+
+    QString filePath = QFileDialog::getOpenFileName(this,
+                                                    tr("Select wkhtmltopdf Executable"),
+                                                    initPath,
+                                                    filter);
+
+    if (!filePath.isEmpty()) {
+        m_wkPathEdit->setText(filePath);
+    }
+}
+
+void VExportDialog::handleInputChanged()
+{
+    // Source.
+    bool sourceOk = true;
+    if (m_srcCB->count() == 0) {
+        sourceOk = false;
+    }
+
+    // Output folder.
+    bool pathOk = true;
+    QString path = getOutputDirectory();
+    if (path.isEmpty() || !VUtils::checkPathLegal(path)) {
+        pathOk = false;
+    }
+
+    m_exportBtn->setEnabled(sourceOk && pathOk);
+    m_openBtn->setEnabled(pathOk);
+}
+
+void VExportDialog::appendLogLine(const QString &p_text)
+{
+    m_consoleEdit->appendPlainText(">>> " + p_text);
+    m_consoleEdit->ensureCursorVisible();
+    QCoreApplication::sendPostedEvents();
+}
+
+int VExportDialog::doExport(VFile *p_file,
+                            const ExportOption &p_opt,
+                            const QString &p_outputFolder,
+                            QString *p_errMsg,
+                            QList<QString> *p_outputFiles)
+{
+    Q_ASSERT(p_file);
+
+    appendLogLine(tr("Exporting note %1.").arg(p_file->fetchPath()));
+
+    int ret = 0;
+    switch (p_opt.m_format) {
+    case ExportFormat::Markdown:
+        ret = doExportMarkdown(p_file, p_opt, p_outputFolder, p_errMsg, p_outputFiles);
+        break;
+
+    case ExportFormat::PDF:
+        V_FALLTHROUGH;
+    case ExportFormat::OnePDF:
+        ret = doExportPDF(p_file, p_opt, p_outputFolder, p_errMsg, p_outputFiles);
+        break;
+
+    case ExportFormat::HTML:
+        ret = doExportHTML(p_file, p_opt, p_outputFolder, p_errMsg, p_outputFiles);
+        break;
+
+    case ExportFormat::Custom:
+        ret = doExportCustom(p_file, p_opt, p_outputFolder, p_errMsg, p_outputFiles);
+        break;
+
+    default:
+        break;
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExport(VDirectory *p_directory,
+                            const ExportOption &p_opt,
+                            const QString &p_outputFolder,
+                            QString *p_errMsg,
+                            QList<QString> *p_outputFiles)
+{
+    Q_ASSERT(p_directory);
+
+    bool opened = p_directory->isOpened();
+    if (!opened && !p_directory->open()) {
+        LOGERR(tr("Fail to open folder %1.").arg(p_directory->fetchRelativePath()));
+        return 0;
+    }
+
+    int ret = 0;
+
+    QString folderName = VUtils::getDirNameWithSequence(p_outputFolder,
+                                                        p_directory->getName());
+    QString outputPath = QDir(p_outputFolder).filePath(folderName);
+    if (!VUtils::makePath(outputPath)) {
+        LOGERR(tr("Fail to create directory %1.").arg(outputPath));
+        goto exit;
+    }
+
+    // Export child notes.
+    for (auto const & file : p_directory->getFiles()) {
+        if (!checkUserAction()) {
+            goto exit;
+        }
+
+        ret += doExport(file, p_opt, outputPath, p_errMsg, p_outputFiles);
+    }
+
+    // Export subfolders.
+    if (p_opt.m_processSubfolders) {
+        for (auto const & dir : p_directory->getSubDirs()) {
+            if (!checkUserAction()) {
+                goto exit;
+            }
+
+            ret += doExport(dir, p_opt, outputPath, p_errMsg, p_outputFiles);
+        }
+    }
+
+exit:
+    if (!opened) {
+        p_directory->close();
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExport(VNotebook *p_notebook,
+                            const ExportOption &p_opt,
+                            const QString &p_outputFolder,
+                            QString *p_errMsg,
+                            QList<QString> *p_outputFiles)
+{
+    Q_ASSERT(p_notebook);
+
+    bool opened = p_notebook->isOpened();
+    if (!opened && !p_notebook->open()) {
+        LOGERR(tr("Fail to open notebook %1.").arg(p_notebook->getName()));
+        return 0;
+    }
+
+    int ret = 0;
+
+    QString folderName = VUtils::getDirNameWithSequence(p_outputFolder,
+                                                        p_notebook->getName());
+    QString outputPath = QDir(p_outputFolder).filePath(folderName);
+    if (!VUtils::makePath(outputPath)) {
+        LOGERR(tr("Fail to create directory %1.").arg(outputPath));
+        goto exit;
+    }
+
+    // Export subfolder.
+    for (auto const & dir : p_notebook->getRootDir()->getSubDirs()) {
+        if (!checkUserAction()) {
+            goto exit;
+        }
+
+        ret += doExport(dir, p_opt, outputPath, p_errMsg, p_outputFiles);
+    }
+
+exit:
+    if (!opened) {
+        p_notebook->close();
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExport(VCart *p_cart,
+                            const ExportOption &p_opt,
+                            const QString &p_outputFolder,
+                            QString *p_errMsg,
+                            QList<QString> *p_outputFiles)
+{
+    Q_UNUSED(p_cart);
+    Q_ASSERT(p_cart);
+    int ret = 0;
+
+    QVector<QString> files = m_cart->getFiles();
+    for (auto const & it : files) {
+        VFile *file = g_vnote->getFile(it);
+        if (!file) {
+            LOGERR(tr("Fail to open file %1.").arg(it));
+            continue;
+        }
+
+        ret += doExport(file, p_opt, p_outputFolder, p_errMsg, p_outputFiles);
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExportMarkdown(VFile *p_file,
+                                    const ExportOption &p_opt,
+                                    const QString &p_outputFolder,
+                                    QString *p_errMsg,
+                                    QList<QString> *p_outputFiles)
+{
+    Q_UNUSED(p_opt);
+
+    QString srcFilePath(p_file->fetchPath());
+
+    if (p_file->getDocType() != DocType::Markdown) {
+        LOGERR(tr("Skip exporting non-Markdown file %1 as Markdown.").arg(srcFilePath));
+        return 0;
+    }
+
+    // Export it to a folder with the same name.
+    QString name = VUtils::getDirNameWithSequence(p_outputFolder, p_file->getName());
+    QString outputPath = QDir(p_outputFolder).filePath(name);
+    if (!VUtils::makePath(outputPath)) {
+        LOGERR(tr("Fail to create directory %1.").arg(outputPath));
+        return 0;
+    }
+
+    // Copy the note file.
+    QString destPath = QDir(outputPath).filePath(p_file->getName());
+    if (!VUtils::copyFile(srcFilePath, destPath, false)) {
+        LOGERR(tr("Fail to copy the note file %1.").arg(srcFilePath));
+        return 0;
+    }
+
+    // Copy images.
+    int ret = 1;
+    int nrImageCopied = 0;
+    QVector<ImageLink> images = VUtils::fetchImagesFromMarkdownFile(p_file,
+                                                                    ImageLink::LocalRelativeInternal);
+    if (!VNoteFile::copyInternalImages(images,
+                                       outputPath,
+                                       false,
+                                       &nrImageCopied,
+                                       p_errMsg)) {
+        ret = 0;
+        appendLogLine(tr("Fail to copy images of note %1.").arg(srcFilePath));
+    }
+
+    // Copy attachments.
+    if (p_file->getType() == FileType::Note) {
+        VNoteFile *noteFile = static_cast<VNoteFile *>(p_file);
+        QString attaFolder = noteFile->getAttachmentFolder();
+        if (!attaFolder.isEmpty()) {
+            QString attaFolderPath;
+            attaFolderPath = noteFile->fetchAttachmentFolderPath();
+            QString relativePath = QDir(noteFile->fetchBasePath()).relativeFilePath(attaFolderPath);
+            QString folderPath = QDir(outputPath).filePath(relativePath);
+
+            // Copy attaFolder to folderPath.
+            if (!VUtils::copyDirectory(attaFolderPath, folderPath, false)) {
+                LOGERR(tr("Fail to copy attachments folder %1 to %2.")
+                         .arg(attaFolderPath).arg(folderPath));
+                ret = 0;
+            }
+        }
+    }
+
+    if (ret) {
+        if (p_outputFiles) {
+            p_outputFiles->append(destPath);
+        }
+
+        appendLogLine(tr("Note %1 exported to %2.").arg(srcFilePath).arg(outputPath));
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExportPDF(VFile *p_file,
+                               const ExportOption &p_opt,
+                               const QString &p_outputFolder,
+                               QString *p_errMsg,
+                               QList<QString> *p_outputFiles)
+{
+    Q_UNUSED(p_opt);
+
+    QString srcFilePath(p_file->fetchPath());
+
+    if (p_file->getDocType() != DocType::Markdown) {
+        LOGERR(tr("Skip exporting non-Markdown file %1 as PDF.").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 = ".pdf";
+    QString name = VUtils::getFileNameWithSequence(p_outputFolder,
+                                                   QFileInfo(p_file->getName()).completeBaseName() + suffix);
+    QString outputPath = QDir(p_outputFolder).filePath(name);
+
+    if (m_exporter->exportPDF(p_file, p_opt, outputPath, p_errMsg)) {
+        if (p_outputFiles) {
+            p_outputFiles->append(outputPath);
+        }
+
+        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;
+    }
+}
+
+int VExportDialog::doExportHTML(VFile *p_file,
+                                const ExportOption &p_opt,
+                                const QString &p_outputFolder,
+                                QString *p_errMsg,
+                                QList<QString> *p_outputFiles)
+{
+    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 = p_opt.m_htmlOpt.m_mimeHTML ? ".mht" : ".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)) {
+        if (p_outputFiles) {
+            p_outputFiles->append(outputPath);
+        }
+
+        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;
+    }
+}
+
+int VExportDialog::doExportCustom(VFile *p_file,
+                                  const ExportOption &p_opt,
+                                  const QString &p_outputFolder,
+                                  QString *p_errMsg,
+                                  QList<QString> *p_outputFiles)
+{
+    Q_UNUSED(p_opt);
+
+    QString srcFilePath(p_file->fetchPath());
+
+    if (p_file->getDocType() != DocType::Markdown) {
+        LOGERR(tr("Skip exporting non-Markdown file %1.").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 = "." + p_opt.m_customOpt.m_outputSuffix;
+    QString name = VUtils::getFileNameWithSequence(p_outputFolder,
+                                                   QFileInfo(p_file->getName()).completeBaseName() + suffix);
+    QString outputPath = QDir(p_outputFolder).filePath(name);
+
+    if (m_exporter->exportCustom(p_file, p_opt, outputPath, p_errMsg)) {
+        if (p_outputFiles) {
+            p_outputFiles->append(outputPath);
+        }
+
+        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) {
+        return false;
+    }
+
+    QCoreApplication::processEvents();
+
+    return true;
+}
+
+void VExportDialog::handleLayoutBtnClicked()
+{
+#ifndef QT_NO_PRINTER
+    QPrinter printer;
+    printer.setPageLayout(m_pageLayout);
+
+    QPageSetupDialog dlg(&printer, this);
+    if (dlg.exec() != QDialog::Accepted) {
+        return;
+    }
+
+    m_pageLayout.setUnits(QPageLayout::Millimeter);
+    m_pageLayout.setPageSize(printer.pageLayout().pageSize());
+    m_pageLayout.setMargins(printer.pageLayout().margins(QPageLayout::Millimeter));
+    m_pageLayout.setOrientation(printer.pageLayout().orientation());
+
+    updatePageLayoutLabel();
+#endif
+}
+
+void VExportDialog::updatePageLayoutLabel()
+{
+    m_layoutLabel->setText(QString("%1, %2").arg(m_pageLayout.pageSize().name())
+                                            .arg(m_pageLayout.orientation() == QPageLayout::Portrait ?
+                                                 tr("Portrait") : tr("Landscape")));
+}
+
+void VExportDialog::handleCurrentFormatChanged(int p_index)
+{
+    bool pdfEnabled = false;
+    bool htmlEnabled = false;
+    bool pdfTitleNameEnabled = false;
+    bool customEnabled = false;
+
+    if (p_index >= 0) {
+        switch (currentFormat()) {
+        case ExportFormat::PDF:
+            pdfEnabled = true;
+            m_wkhtmltopdfCB->setEnabled(true);
+            break;
+
+        case ExportFormat::HTML:
+            htmlEnabled = true;
+            break;
+
+        case ExportFormat::OnePDF:
+            pdfEnabled = true;
+            pdfTitleNameEnabled = true;
+            m_wkhtmltopdfCB->setChecked(true);
+            m_wkhtmltopdfCB->setEnabled(false);
+            break;
+
+        case ExportFormat::Custom:
+            customEnabled = true;
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    m_pdfSettings->setVisible(pdfEnabled);
+    m_htmlSettings->setVisible(htmlEnabled);
+    m_customSettings->setVisible(customEnabled);
+
+    m_wkTitleEdit->setEnabled(pdfTitleNameEnabled);
+    m_wkTargetFileNameEdit->setEnabled(pdfTitleNameEnabled);
+
+    QTimer::singleShot(100, [this]() {
+                resize(size().width(), minimumSizeHint().height());
+            });
+}
+
+void VExportDialog::handleCurrentSrcChanged(int p_index)
+{
+    bool subfolderEnabled = false;
+
+    if (p_index >= 0) {
+        switch (currentSource()) {
+        case ExportSource::CurrentFolder:
+            subfolderEnabled = true;
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    m_subfolderCB->setVisible(subfolderEnabled);
+}
+
+int VExportDialog::doExportPDFAllInOne(const QList<QString> &p_files,
+                                       const ExportOption &p_opt,
+                                       const QString &p_outputFolder,
+                                       QString *p_errMsg)
+{
+    if (p_files.isEmpty()) {
+        return 0;
+    }
+
+    if (!VUtils::makePath(p_outputFolder)) {
+        LOGERR(tr("Fail to create directory %1.").arg(p_outputFolder));
+        return 0;
+    }
+
+    // Get output file.
+    const QString suffix = ".pdf";
+    QString name = p_opt.m_pdfOpt.m_wkTargetFileName;
+    if (name.isEmpty()) {
+        name = VUtils::getFileNameWithSequence(p_outputFolder,
+                                               QFileInfo(p_files.first()).completeBaseName() + suffix);
+    } else if (!name.endsWith(suffix)) {
+        name += suffix;
+    }
+
+    QString outputPath = QDir(p_outputFolder).filePath(name);
+
+    int ret = m_exporter->exportPDFInOne(p_files, p_opt, outputPath, p_errMsg);
+    if (ret > 0) {
+        appendLogLine(tr("%1 notes exported to %2.").arg(ret).arg(outputPath));
+    } else {
+        appendLogLine(tr("Fail to export %1 notes in one PDF.").arg(p_files.size()));
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExportCustomAllInOne(const QList<QString> &p_files,
+                                          const ExportOption &p_opt,
+                                          const QString &p_outputFolder,
+                                          QString *p_errMsg)
+{
+    if (p_files.isEmpty()) {
+        return 0;
+    }
+
+    if (!VUtils::makePath(p_outputFolder)) {
+        LOGERR(tr("Fail to create directory %1.").arg(p_outputFolder));
+        return 0;
+    }
+
+    // Get output file.
+    QString suffix = "." + p_opt.m_customOpt.m_outputSuffix;
+    QString name = p_opt.m_customOpt.m_targetFileName;
+    if (name.isEmpty()) {
+        name = VUtils::getFileNameWithSequence(p_outputFolder,
+                                               QFileInfo(p_files.first()).completeBaseName() + suffix);
+    } else if (!name.endsWith(suffix)) {
+        name += suffix;
+    }
+
+    QString outputPath = QDir(p_outputFolder).filePath(name);
+
+    int ret = m_exporter->exportCustomInOne(p_files, p_opt, outputPath, p_errMsg);
+    if (ret > 0) {
+        appendLogLine(tr("%1 notes exported to %2.").arg(ret).arg(outputPath));
+    } else {
+        appendLogLine(tr("Fail to export %1 notes in one.").arg(p_files.size()));
+    }
+
+    return ret;
+}
+
+QWidget *VExportDialog::setupCustomAdvancedSettings()
+{
+    // Source format.
+    m_customSrcFormatCB = VUtils::getComboBox(this);
+    m_customSrcFormatCB->setToolTip(tr("Choose format of the input"));
+
+    // Output suffix.
+    m_customSuffixEdit = new VLineEdit(this);
+    m_customSuffixEdit->setPlaceholderText(tr("Without the preceding dot"));
+    m_customSuffixEdit->setToolTip(tr("Suffix of the output file without the preceding dot"));
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_customSuffixEdit);
+    m_customSuffixEdit->setValidator(validator);
+
+    QLabel *tipsLabel = new QLabel(tr("<span><span style=\"font-weight:bold;\">%0</span> for the input file; "
+                                      "<span style=\"font-weight:bold;\">%1</span> for the output file; "
+                                      "<span style=\"font-weight:bold;\">%2</span> for the rendering CSS style file; "
+                                      "<span style=\"font-weight:bold;\">%3</span> for the input file directory; "
+                                      "<span style=\"font-weight:bold;\">%4</span> for the rendering code block CSS style file.</span>"),
+                                   this);
+    tipsLabel->setWordWrap(true);
+
+    // Enable All In One.
+    m_customAllInOneCB = new QCheckBox(tr("Enable All In One"), this);
+    m_customAllInOneCB->setToolTip(tr("Pass a list of input files to the custom command"));
+    connect(m_customAllInOneCB, &QCheckBox::stateChanged,
+            this, [this](int p_state) {
+                bool checked = p_state == Qt::Checked;
+                m_customFolderSepEdit->setEnabled(checked);
+                m_customTargetFileNameEdit->setEnabled(checked);
+            });
+
+    // Enable PDF-like.
+    m_customPdfLikeCB = new QCheckBox(tr("PDF-Like"), this);
+    m_customPdfLikeCB->setToolTip(tr("Treat the exported file as PDF, such as wrapping line"));
+
+    // Input directory separator.
+    m_customFolderSepEdit = new VLineEdit(this);
+    m_customFolderSepEdit->setPlaceholderText(tr("Separator to concatenate input files directories"));
+    m_customFolderSepEdit->setToolTip(tr("Separator to concatenate input files directories"));
+    m_customFolderSepEdit->setEnabled(false);
+
+    // Target file name for all in one.
+    m_customTargetFileNameEdit = new VLineEdit(this);
+    m_customTargetFileNameEdit->setPlaceholderText(tr("Empty to use the name of the first source file"));
+    m_customTargetFileNameEdit->setToolTip(tr("Name of the generated All-In-One file"));
+    validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_customTargetFileNameEdit);
+    m_customTargetFileNameEdit->setValidator(validator);
+    m_customTargetFileNameEdit->setEnabled(false);
+
+    // Cmd edit.
+    m_customCmdEdit = new QPlainTextEdit(this);
+    m_customCmdEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
+    QString cmdExamp("pandoc --resource-path=.:\"%3\" --css=\"%2\" --css=\"%4\" -s -o \"%1\" \"%0\"");
+    m_customCmdEdit->setPlaceholderText(cmdExamp);
+    m_customCmdEdit->setToolTip(tr("Custom command to be executed"));
+    m_customCmdEdit->setProperty("LineEdit", true);
+
+    QGridLayout *advLayout = new QGridLayout();
+    advLayout->addWidget(new QLabel(tr("Source format:")), 0, 0);
+    advLayout->addWidget(m_customSrcFormatCB, 0, 1, 1, 2);
+
+    advLayout->addWidget(new QLabel(tr("Output suffix:")), 0, 3);
+    advLayout->addWidget(m_customSuffixEdit, 0, 4, 1, 2);
+
+    advLayout->addWidget(m_customAllInOneCB, 1, 1, 1, 2);
+    advLayout->addWidget(m_customPdfLikeCB, 1, 4, 1, 2);
+
+    advLayout->addWidget(new QLabel(tr("Output file name:")), 2, 0);
+    advLayout->addWidget(m_customTargetFileNameEdit, 2, 1, 1, 2);
+
+    advLayout->addWidget(new QLabel(tr("Input directories separator:")), 2, 3);
+    advLayout->addWidget(m_customFolderSepEdit, 2, 4, 1, 2);
+
+    advLayout->addWidget(tipsLabel, 3, 0, 1, 6);
+
+    advLayout->addWidget(m_customCmdEdit, 4, 0, 1, 6);
+
+    advLayout->setContentsMargins(0, 0, 0, 0);
+
+    QWidget *wid = new QWidget();
+    wid->setLayout(advLayout);
+
+    m_customCmdEdit->setMaximumHeight(m_customSrcFormatCB->height() * 3);
+
+    return wid;
+}
+
+int VExportDialog::outputAsHTML(const QString &p_outputFolder,
+                                QString *p_errMsg,
+                                QList<QString> *p_outputFiles)
+{
+    int ret = 0;
+    ExportFormat fmt = s_opt.m_format;
+    s_opt.m_format = ExportFormat::HTML;
+    switch (s_opt.m_source) {
+    case ExportSource::CurrentNote:
+        ret = doExport(m_file, s_opt, p_outputFolder, p_errMsg, p_outputFiles);
+        break;
+
+    case ExportSource::CurrentFolder:
+        ret = doExport(m_directory, s_opt, p_outputFolder, p_errMsg, p_outputFiles);
+        break;
+
+    case ExportSource::CurrentNotebook:
+        ret = doExport(m_notebook, s_opt, p_outputFolder, p_errMsg, p_outputFiles);
+        break;
+
+    case ExportSource::Cart:
+        ret = doExport(m_cart, s_opt, p_outputFolder, p_errMsg, p_outputFiles);
+        break;
+
+    default:
+        break;
+    }
+
+    s_opt.m_format = fmt;
+
+    return ret;
+}
+
+QList<QString> VExportDialog::collectFiles(QString *p_errMsg)
+{
+    Q_UNUSED(p_errMsg);
+
+    QList<QString> files;
+    switch (s_opt.m_source) {
+    case ExportSource::CurrentNote:
+        files.append(m_file->fetchPath());
+        break;
+
+    case ExportSource::CurrentFolder:
+        files = m_directory->collectFiles();
+        break;
+
+    case ExportSource::CurrentNotebook:
+        files = m_notebook->collectFiles();
+        break;
+
+    case ExportSource::Cart:
+        files = m_cart->getFiles().toList();
+        break;
+
+    default:
+        break;
+    }
+
+    return files;
+}

+ 516 - 0
src/dialog/vexportdialog.h

@@ -0,0 +1,516 @@
+#ifndef VEXPORTDIALOG_H
+#define VEXPORTDIALOG_H
+
+#include <QDialog>
+#include <QPageLayout>
+#include <QList>
+#include <QComboBox>
+
+#include "vconstants.h"
+
+class QLabel;
+class VLineEdit;
+class QDialogButtonBox;
+class QPushButton;
+class QGroupBox;
+class QPlainTextEdit;
+class VNotebook;
+class VDirectory;
+class VFile;
+class VCart;
+class VExporter;
+class QCheckBox;
+class VLineEdit;
+class QProgressBar;
+
+
+enum class ExportSource
+{
+    CurrentNote = 0,
+    CurrentFolder,
+    CurrentNotebook,
+    Cart
+};
+
+
+enum class ExportFormat
+{
+    Markdown = 0,
+    HTML,
+    PDF,
+    OnePDF,
+    Custom
+};
+
+
+enum class ExportPageNumber
+{
+    None = 0,
+    Left,
+    Center,
+    Right
+};
+
+
+struct ExportHTMLOption
+{
+    ExportHTMLOption()
+        : m_embedCssStyle(true),
+          m_completeHTML(true),
+          m_embedImages(true),
+          m_mimeHTML(false),
+          m_outlinePanel(true)
+    {
+    }
+
+    ExportHTMLOption(bool p_embedCssStyle,
+                     bool p_completeHTML,
+                     bool p_embedImages,
+                     bool p_mimeHTML,
+                     bool p_outlinePanel)
+        : m_embedCssStyle(p_embedCssStyle),
+          m_completeHTML(p_completeHTML),
+          m_embedImages(p_embedImages),
+          m_mimeHTML(p_mimeHTML),
+          m_outlinePanel(p_outlinePanel)
+    {
+    }
+
+    bool m_embedCssStyle;
+    bool m_completeHTML;
+    bool m_embedImages;
+    bool m_mimeHTML;
+    bool m_outlinePanel;
+};
+
+
+struct ExportPDFOption
+{
+    ExportPDFOption()
+        : m_layout(NULL),
+          m_wkhtmltopdf(false),
+          m_wkEnableBackground(true),
+          m_enableTableOfContents(false),
+          m_wkPageNumber(ExportPageNumber::None)
+    {
+    }
+
+    ExportPDFOption(QPageLayout *p_layout,
+                    bool p_wkhtmltopdf,
+                    const QString &p_wkPath,
+                    bool p_wkEnableBackground,
+                    bool p_enableTableOfContents,
+                    const QString &p_wkTitle,
+                    const QString &p_wkTargetFileName,
+                    ExportPageNumber p_wkPageNumber,
+                    const QString &p_wkExtraArgs)
+        : m_layout(p_layout),
+          m_wkhtmltopdf(p_wkhtmltopdf),
+          m_wkPath(p_wkPath),
+          m_wkEnableBackground(p_wkEnableBackground),
+          m_enableTableOfContents(p_enableTableOfContents),
+          m_wkTitle(p_wkTitle),
+          m_wkTargetFileName(p_wkTargetFileName),
+          m_wkPageNumber(p_wkPageNumber),
+          m_wkExtraArgs(p_wkExtraArgs)
+    {
+    }
+
+    QPageLayout *m_layout;
+    bool m_wkhtmltopdf;
+    QString m_wkPath;
+    bool m_wkEnableBackground;
+    bool m_enableTableOfContents;;
+    QString m_wkTitle;
+    QString m_wkTargetFileName;
+    ExportPageNumber m_wkPageNumber;
+    QString m_wkExtraArgs;
+};
+
+
+struct ExportCustomOption
+{
+#if defined(Q_OS_WIN)
+    #define DEFAULT_SEP ";"
+#else
+    #define DEFAULT_SEP ":"
+#endif
+
+    enum SourceFormat
+    {
+        Markdown = 0,
+        HTML
+    };
+
+    ExportCustomOption()
+        : m_srcFormat(SourceFormat::Markdown),
+          m_allInOne(false),
+          m_pdfLike(false),
+          m_folderSep(DEFAULT_SEP)
+    {
+    }
+
+    ExportCustomOption(const QStringList &p_config)
+        : m_srcFormat(SourceFormat::Markdown),
+          m_allInOne(false),
+          m_pdfLike(false),
+          m_folderSep(DEFAULT_SEP)
+    {
+        if (p_config.size() < 3) {
+            return;
+        }
+
+        if (p_config.at(0).trimmed() != "0") {
+            m_srcFormat = SourceFormat::HTML;
+        }
+
+        m_outputSuffix = p_config.at(1).trimmed();
+
+        m_cmd = p_config.at(2).trimmed();
+    }
+
+    ExportCustomOption(ExportCustomOption::SourceFormat p_srcFormat,
+                       const QString &p_outputSuffix,
+                       const QString &p_cmd,
+                       const QString &p_cssUrl,
+                       const QString &p_codeBlockCssUrl,
+                       bool p_allInOne,
+                       bool p_pdfLike,
+                       const QString &p_folderSep,
+                       const QString &p_targetFileName)
+        : m_srcFormat(p_srcFormat),
+          m_outputSuffix(p_outputSuffix),
+          m_cssUrl(p_cssUrl),
+          m_codeBlockCssUrl(p_codeBlockCssUrl),
+          m_allInOne(p_allInOne),
+          m_pdfLike(p_pdfLike),
+          m_folderSep(p_folderSep),
+          m_targetFileName(p_targetFileName)
+    {
+        QStringList cmds = p_cmd.split('\n');
+        if (!cmds.isEmpty()) {
+            m_cmd = cmds.first();
+        }
+    }
+
+    QStringList toConfig() const
+    {
+        QStringList config;
+        config << QString::number((int)m_srcFormat);
+        config << m_outputSuffix;
+        config << m_cmd;
+
+        return config;
+    }
+
+    SourceFormat m_srcFormat;
+    QString m_outputSuffix;
+    QString m_cmd;
+
+    QString m_cssUrl;
+    QString m_codeBlockCssUrl;
+
+    bool m_allInOne;
+    bool m_pdfLike;
+
+    QString m_folderSep;
+    QString m_targetFileName;
+};
+
+
+struct ExportOption
+{
+    ExportOption()
+        : m_source(ExportSource::CurrentNote),
+          m_format(ExportFormat::Markdown),
+          m_renderer(MarkdownConverterType::MarkdownIt),
+          m_processSubfolders(true)
+    {
+    }
+
+    ExportOption(ExportSource p_source,
+                 ExportFormat p_format,
+                 MarkdownConverterType p_renderer,
+                 const QString &p_renderBg,
+                 const QString &p_renderStyle,
+                 const QString &p_renderCodeBlockStyle,
+                 bool p_processSubfolders,
+                 const ExportPDFOption &p_pdfOpt,
+                 const ExportHTMLOption &p_htmlOpt,
+                 const ExportCustomOption &p_customOpt)
+        : m_source(p_source),
+          m_format(p_format),
+          m_renderer(p_renderer),
+          m_renderBg(p_renderBg),
+          m_renderStyle(p_renderStyle),
+          m_renderCodeBlockStyle(p_renderCodeBlockStyle),
+          m_processSubfolders(p_processSubfolders),
+          m_pdfOpt(p_pdfOpt),
+          m_htmlOpt(p_htmlOpt),
+          m_customOpt(p_customOpt)
+    {
+    }
+
+    ExportSource m_source;
+    ExportFormat m_format;
+    MarkdownConverterType m_renderer;
+
+    // Background name.
+    QString m_renderBg;
+
+    QString m_renderStyle;
+    QString m_renderCodeBlockStyle;
+
+    // Whether process subfolders recursively when source is CurrentFolder.
+    bool m_processSubfolders;
+
+    ExportPDFOption m_pdfOpt;
+
+    ExportHTMLOption m_htmlOpt;
+
+    ExportCustomOption m_customOpt;
+};
+
+
+class VExportDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    VExportDialog(VNotebook *p_notebook,
+                  VDirectory *p_directory,
+                  VFile *p_file,
+                  VCart *p_cart,
+                  MarkdownConverterType p_renderer,
+                  QWidget *p_parent = nullptr);
+
+private slots:
+    void startExport();
+
+    void handleOutputBrowseBtnClicked();
+
+    void handleWkPathBrowseBtnClicked();
+
+    void handleInputChanged();
+
+    void handleLayoutBtnClicked();
+
+    void handleCurrentFormatChanged(int p_index);
+
+    void handleCurrentSrcChanged(int p_index);
+
+private:
+    void setupUI();
+
+    QWidget *setupPDFAdvancedSettings();
+
+    QWidget *setupHTMLAdvancedSettings();
+
+    QWidget *setupGeneralAdvancedSettings();
+
+    QWidget *setupCustomAdvancedSettings();
+
+    void initUIFields(MarkdownConverterType p_renderer);
+
+    QString getOutputDirectory() const;
+
+    void appendLogLine(const QString &p_text);
+
+    // Return number of files exported.
+    int doExport(VFile *p_file,
+                 const ExportOption &p_opt,
+                 const QString &p_outputFolder,
+                 QString *p_errMsg = NULL,
+                 QList<QString> *p_outputFiles = NULL);
+
+    int doExport(VDirectory *p_directory,
+                 const ExportOption &p_opt,
+                 const QString &p_outputFolder,
+                 QString *p_errMsg = NULL,
+                 QList<QString> *p_outputFiles = NULL);
+
+    int doExport(VNotebook *p_notebook,
+                 const ExportOption &p_opt,
+                 const QString &p_outputFolder,
+                 QString *p_errMsg = NULL,
+                 QList<QString> *p_outputFiles = NULL);
+
+    int doExport(VCart *p_cart,
+                 const ExportOption &p_opt,
+                 const QString &p_outputFolder,
+                 QString *p_errMsg = NULL,
+                 QList<QString> *p_outputFiles = NULL);
+
+    int doExportMarkdown(VFile *p_file,
+                         const ExportOption &p_opt,
+                         const QString &p_outputFolder,
+                         QString *p_errMsg = NULL,
+                         QList<QString> *p_outputFiles = NULL);
+
+    int doExportPDF(VFile *p_file,
+                    const ExportOption &p_opt,
+                    const QString &p_outputFolder,
+                    QString *p_errMsg = NULL,
+                    QList<QString> *p_outputFiles = NULL);
+
+    int doExportHTML(VFile *p_file,
+                     const ExportOption &p_opt,
+                     const QString &p_outputFolder,
+                     QString *p_errMsg = NULL,
+                     QList<QString> *p_outputFiles = NULL);
+
+    int doExportPDFAllInOne(const QList<QString> &p_files,
+                            const ExportOption &p_opt,
+                            const QString &p_outputFolder,
+                            QString *p_errMsg = NULL);
+
+    int doExportCustomAllInOne(const QList<QString> &p_files,
+                               const ExportOption &p_opt,
+                               const QString &p_outputFolder,
+                               QString *p_errMsg = NULL);
+
+    int doExportCustom(VFile *p_file,
+                       const ExportOption &p_opt,
+                       const QString &p_outputFolder,
+                       QString *p_errMsg = NULL,
+                       QList<QString> *p_outputFiles = NULL);
+
+    // Return false if we could not continue.
+    bool checkUserAction();
+
+    void updatePageLayoutLabel();
+
+    bool checkWkhtmltopdfExecutable(const QString &p_file);
+
+    ExportSource currentSource() const;
+
+    ExportFormat currentFormat() const;
+
+    int outputAsHTML(const QString &p_outputFolder,
+                     QString *p_errMsg = NULL,
+                     QList<QString> *p_outputFiles = NULL);
+
+    // Collect files to be handled.
+    QList<QString> collectFiles(QString *p_errMsg = NULL);
+
+    QComboBox *m_srcCB;
+
+    QComboBox *m_formatCB;
+
+    QComboBox *m_rendererCB;
+
+    QComboBox *m_renderBgCB;
+
+    QComboBox *m_renderStyleCB;
+
+    QComboBox *m_renderCodeBlockStyleCB;
+
+    VLineEdit *m_outputEdit;
+
+    QGroupBox *m_basicBox;
+
+    QGroupBox *m_settingBox;
+
+    QWidget *m_pdfSettings;
+
+    QWidget *m_htmlSettings;
+
+    QWidget *m_generalSettings;
+
+    QWidget *m_customSettings;
+
+    QPlainTextEdit *m_consoleEdit;
+
+    QDialogButtonBox *m_btnBox;
+
+    QPushButton *m_openBtn;
+
+    QPushButton *m_exportBtn;
+
+    QPushButton *m_copyBtn;
+
+    QLabel *m_layoutLabel;
+
+    QCheckBox *m_wkhtmltopdfCB;
+
+    VLineEdit *m_wkPathEdit;
+
+    QPushButton *m_wkPathBrowseBtn;
+
+    VLineEdit *m_wkTitleEdit;
+
+    VLineEdit *m_wkTargetFileNameEdit;
+
+    QCheckBox *m_wkBackgroundCB;
+
+    QCheckBox *m_tableOfContentsCB;
+
+    QComboBox *m_wkPageNumberCB;
+
+    VLineEdit *m_wkExtraArgsEdit;
+
+    QCheckBox *m_embedStyleCB;
+
+    QCheckBox *m_completeHTMLCB;
+
+    QCheckBox *m_embedImagesCB;
+
+    QCheckBox *m_mimeHTMLCB;
+
+    QCheckBox *m_outlinePanelCB;
+
+    QCheckBox *m_subfolderCB;
+
+    QComboBox *m_customSrcFormatCB;
+
+    VLineEdit *m_customSuffixEdit;
+
+    QCheckBox *m_customAllInOneCB;
+
+    QCheckBox *m_customPdfLikeCB;
+
+    QPlainTextEdit *m_customCmdEdit;
+
+    VLineEdit *m_customFolderSepEdit;
+
+    VLineEdit *m_customTargetFileNameEdit;
+
+    VNotebook *m_notebook;
+
+    VDirectory *m_directory;
+
+    VFile *m_file;
+
+    VCart *m_cart;
+
+    QProgressBar *m_proBar;
+
+    QPageLayout m_pageLayout;
+
+    // Whether we are exporting files.
+    bool m_inExport;
+
+    // Asked to stop exporting by user.
+    bool m_askedToStop;
+
+    // Exporter used to export PDF and HTML.
+    VExporter *m_exporter;
+
+    // Last exproted file path.
+    QString m_exportedFile;
+
+    // Last output folder path.
+    static QString s_lastOutputFolder;
+
+    static ExportOption s_opt;
+};
+
+inline ExportSource VExportDialog::currentSource() const
+{
+    return (ExportSource)m_srcCB->currentData().toInt();
+}
+
+inline ExportFormat VExportDialog::currentFormat() const
+{
+    return (ExportFormat)m_formatCB->currentData().toInt();
+}
+#endif // VEXPORTDIALOG_H

+ 6 - 2
src/src.pro

@@ -161,7 +161,9 @@ SOURCES += main.cpp\
     vwebview.cpp \
     websocketclientwrapper.cpp \
     websockettransport.cpp \
-    vmathjaxwebdocument.cpp
+    vmathjaxwebdocument.cpp \
+    vexporter.cpp \
+    dialog/vexportdialog.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -307,7 +309,9 @@ HEADERS  += vmainwindow.h \
     vwebview.h \
     websocketclientwrapper.h \
     websockettransport.h \
-    vmathjaxwebdocument.h
+    vmathjaxwebdocument.h \
+    vexporter.h \
+    dialog/vexportdialog.h
 
 RESOURCES += \
     vnote.qrc \

+ 2 - 1
src/utils/vutils.cpp

@@ -664,6 +664,7 @@ QString VUtils::generateHtmlTemplate(MarkdownConverterType p_conType,
                                      const QString &p_renderBg,
                                      const QString &p_renderStyle,
                                      const QString &p_renderCodeBlockStyle,
+                                     quint16 p_port,
                                      bool p_isPDF,
                                      bool p_wkhtmltopdf,
                                      bool p_addToc)
@@ -675,7 +676,7 @@ QString VUtils::generateHtmlTemplate(MarkdownConverterType p_conType,
                                                 g_config->getCodeBlockCssStyleUrl(p_renderCodeBlockStyle),
                                                 p_isPDF);
 
-    return generateHtmlTemplate(templ, p_conType, 0, p_isPDF, p_wkhtmltopdf, p_addToc);
+    return generateHtmlTemplate(templ, p_conType, p_port, p_isPDF, p_wkhtmltopdf, p_addToc);
 }
 
 QString VUtils::generateHtmlTemplate(const QString &p_template,

+ 1 - 0
src/utils/vutils.h

@@ -194,6 +194,7 @@ public:
                                         const QString &p_renderBg,
                                         const QString &p_renderStyle,
                                         const QString &p_renderCodeBlockStyle,
+                                        quint16 p_port,
                                         bool p_isPDF,
                                         bool p_wkhtmltopdf = false,
                                         bool p_addToc = false);

+ 4 - 0
src/vconstants.h

@@ -18,6 +18,10 @@ enum class FileType { Note, Orphan };
 
 enum class ClipboardOpType { CopyFile, CopyDir, Invalid };
 
+enum WebSocketPort { PreviewHelperPort = 10900,
+                     ExportWebViewPort,
+                     LastSpecialPort };
+
 namespace ClipboardConfig
 {
     static const QString c_type = "type";

+ 1063 - 0
src/vexporter.cpp

@@ -0,0 +1,1063 @@
+#include "vexporter.h"
+
+#include <QDebug>
+#include <QWidget>
+#include <QWebChannel>
+// #include <QWebEngineProfile>
+#include <QRegExp>
+#include <QProcess>
+#include <QTemporaryDir>
+#include <QScopedPointer>
+#include <QCoreApplication>
+
+#include "vconfigmanager.h"
+#include "vfile.h"
+#include "vwebview.h"
+#include "utils/vutils.h"
+#include "vpreviewpage.h"
+#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),
+      m_state(ExportState::Idle),
+      m_askedToStop(false)
+{
+}
+
+static QString marginToStrMM(qreal p_margin)
+{
+    return QString("%1mm").arg(p_margin);
+}
+
+void VExporter::prepareExport(const ExportOption &p_opt)
+{
+    bool isPdf = p_opt.m_format == ExportFormat::PDF
+                 || p_opt.m_format == ExportFormat::OnePDF
+                 || (p_opt.m_format == ExportFormat::Custom
+                     && p_opt.m_customOpt.m_pdfLike);
+    bool extraToc = isPdf
+                    && !p_opt.m_pdfOpt.m_wkhtmltopdf
+                    && p_opt.m_pdfOpt.m_enableTableOfContents;
+
+    m_htmlTemplate = VUtils::generateHtmlTemplate(p_opt.m_renderer,
+                                                  p_opt.m_renderBg,
+                                                  p_opt.m_renderStyle,
+                                                  p_opt.m_renderCodeBlockStyle,
+                                                  WebSocketPort::ExportWebViewPort,
+                                                  isPdf,
+                                                  isPdf && p_opt.m_pdfOpt.m_wkhtmltopdf,
+                                                  extraToc);
+
+    bool outline = p_opt.m_htmlOpt.m_outlinePanel
+                   && !isPdf
+                   && (p_opt.m_format == ExportFormat::HTML
+                       || p_opt.m_format == ExportFormat::Custom);
+    m_exportHtmlTemplate = VUtils::generateExportHtmlTemplate(p_opt.m_renderBg,
+                                                              isPdf && p_opt.m_pdfOpt.m_wkhtmltopdf,
+                                                              outline);
+
+    m_pageLayout = *(p_opt.m_pdfOpt.m_layout);
+
+    prepareWKArguments(p_opt.m_pdfOpt);
+}
+
+void VExporter::prepareWKArguments(const ExportPDFOption &p_opt)
+{
+    m_wkArgs.clear();
+    m_wkArgs << "--page-size" << m_pageLayout.pageSize().key();
+    m_wkArgs << "--orientation"
+             << (m_pageLayout.orientation() == QPageLayout::Portrait ? "Portrait" : "Landscape");
+
+    QMarginsF marginsMM = m_pageLayout.margins(QPageLayout::Millimeter);
+    m_wkArgs << "--margin-bottom" << marginToStrMM(marginsMM.bottom());
+    m_wkArgs << "--margin-left" << marginToStrMM(marginsMM.left());
+    m_wkArgs << "--margin-right" << marginToStrMM(marginsMM.right());
+    m_wkArgs << "--margin-top" << marginToStrMM(marginsMM.top());
+
+    QString footer;
+    switch (p_opt.m_wkPageNumber) {
+    case ExportPageNumber::Left:
+        footer = "--footer-left";
+        break;
+
+    case ExportPageNumber::Center:
+        footer = "--footer-center";
+        break;
+
+    case ExportPageNumber::Right:
+        footer = "--footer-right";
+        break;
+
+    default:
+        break;
+    }
+
+    if (!footer.isEmpty()) {
+        m_wkArgs << footer << "[page]"
+                 << "--footer-spacing" << QString::number(marginsMM.bottom() / 3, 'f', 2);
+    }
+
+    // Title.
+    if (!p_opt.m_wkTitle.isEmpty()) {
+        m_wkArgs << "--title" << p_opt.m_wkTitle;
+    }
+
+    m_wkArgs << "--encoding" << "utf-8";
+    m_wkArgs << (p_opt.m_wkEnableBackground ? "--background" : "--no-background");
+
+    // Delay for MathJax.
+    if (p_opt.m_wkhtmltopdf) {
+        m_wkArgs << "--javascript-delay" << "10000";
+    }
+
+    // Append additional global option.
+    if (!p_opt.m_wkExtraArgs.isEmpty()) {
+        m_wkArgs.append(VUtils::parseCombinedArgString(p_opt.m_wkExtraArgs));
+    }
+
+    // TOC option.
+    if (p_opt.m_enableTableOfContents) {
+        m_wkArgs << "toc" << "--toc-text-size-shrink" << "1.0";
+    }
+}
+
+bool VExporter::exportPDF(VFile *p_file,
+                          const ExportOption &p_opt,
+                          const QString &p_outputFile,
+                          QString *p_errMsg)
+{
+    return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg);
+}
+
+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);
+}
+
+static void replaceArgument(QString &p_cmd, const QString &p_arg, const QString &p_val)
+{
+    if (p_val.startsWith("\"")) {
+        // Check if the arg has been already surrounded by ".
+        int pos = 0;
+        while (pos < p_cmd.size()) {
+            int idx = p_cmd.indexOf(p_arg, pos);
+            if (idx == -1) {
+                break;
+            }
+
+            int len = p_arg.size();
+            int nidx = idx;
+            if (idx > 0 && p_cmd[idx - 1] == '"') {
+                --nidx;
+                len += 1;
+            }
+
+            if (idx + p_arg.size() < p_cmd.size()
+                && p_cmd[idx + p_arg.size()] == '"') {
+                len += 1;
+            }
+
+            p_cmd.replace(nidx, len, p_val);
+            pos = nidx + p_val.size() - len;
+        }
+    } else {
+        p_cmd.replace(p_arg, p_val);
+    }
+}
+
+static QString evaluateCommand(const ExportCustomOption &p_opt,
+                               const QString &p_input,
+                               const QString &p_inputFolder,
+                               const QString &p_output)
+{
+    QString cmd(p_opt.m_cmd);
+    replaceArgument(cmd, "%0", p_input);
+    replaceArgument(cmd, "%1", p_output);
+    replaceArgument(cmd, "%2", QDir::toNativeSeparators(p_opt.m_cssUrl));
+    replaceArgument(cmd, "%3", p_inputFolder);
+    replaceArgument(cmd, "%4", QDir::toNativeSeparators(p_opt.m_codeBlockCssUrl));
+
+    return cmd;
+}
+
+bool VExporter::exportCustom(VFile *p_file,
+                             const ExportOption &p_opt,
+                             const QString &p_outputFile,
+                             QString *p_errMsg)
+{
+    const ExportCustomOption &customOpt = p_opt.m_customOpt;
+    if (customOpt.m_srcFormat == ExportCustomOption::Markdown) {
+        // Use Markdown file as input.
+        QList<QString> files;
+        files.append(QDir::toNativeSeparators(p_file->fetchPath()));
+        return convertFilesViaCustom(files, p_outputFile, customOpt, p_errMsg);
+    } else {
+        return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg);
+    }
+}
+
+void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt)
+{
+    Q_ASSERT(!m_webViewer);
+
+    m_webViewer = new VWebView(p_file, static_cast<QWidget *>(parent()));
+    m_webViewer->hide();
+
+    VPreviewPage *page = new VPreviewPage(m_webViewer);
+    m_webViewer->setPage(page);
+    connect(page, &VPreviewPage::loadFinished,
+            this, &VExporter::handleLoadFinished);
+    /*
+    connect(page->profile(), &QWebEngineProfile::downloadRequested,
+            this, &VExporter::handleDownloadRequested);
+    */
+
+    m_webDocument = new VDocument(p_file, m_webViewer);
+    connect(m_webDocument, &VDocument::logicsFinished,
+            this, &VExporter::handleLogicsFinished);
+
+    quint16 port = WebSocketPort::ExportWebViewPort;
+    m_webViewer->bindToChannel(port, QStringLiteral("content"), m_webDocument);
+
+    // Need to generate HTML using Hoedown.
+    if (p_opt.m_renderer == MarkdownConverterType::Hoedown) {
+        VMarkdownConverter mdConverter;
+        QString toc;
+        QString html = mdConverter.generateHtml(p_file->getContent(),
+                                                g_config->getMarkdownExtensions(),
+                                                toc);
+        bool isPdf = p_opt.m_format == ExportFormat::PDF
+                     || p_opt.m_format == ExportFormat::OnePDF;
+        bool extraToc = isPdf
+                        && !p_opt.m_pdfOpt.m_wkhtmltopdf
+                        && p_opt.m_pdfOpt.m_enableTableOfContents;
+        if (extraToc && !toc.isEmpty()) {
+            // Add toc to html.
+            QString div = "<div class=\"vnote-toc\">" + toc + "</div>\n";
+            html = div + html;
+        }
+
+        m_webDocument->setHtml(html);
+    }
+
+    m_baseUrl = p_file->getBaseUrl();
+    m_webViewer->setHtml(m_htmlTemplate, m_baseUrl);
+}
+
+void VExporter::handleLogicsFinished()
+{
+    Q_ASSERT(!(m_noteState & NoteState::WebLogicsReady));
+    m_noteState = NoteState(m_noteState | NoteState::WebLogicsReady);
+}
+
+void VExporter::handleLoadFinished(bool p_ok)
+{
+    Q_ASSERT(!(m_noteState & NoteState::WebLoadFinished));
+    m_noteState = NoteState(m_noteState | NoteState::WebLoadFinished);
+
+    if (!p_ok) {
+        m_noteState = NoteState(m_noteState | NoteState::Failed);
+    }
+}
+
+void VExporter::clearWebViewer()
+{
+    // m_webDocument will be freeed by QObject.
+    delete m_webViewer;
+    m_webViewer = NULL;
+    m_webDocument = NULL;
+    m_baseUrl.clear();
+}
+
+bool VExporter::exportToPDF(VWebView *p_webViewer,
+                            const QString &p_filePath,
+                            const QPageLayout &p_layout)
+{
+/*
+    int pdfPrinted = 0;
+    p_webViewer->page()->printToPdf([&, this](const QByteArray &p_result) {
+        if (p_result.isEmpty() || this->m_state == ExportState::Cancelled) {
+            pdfPrinted = -1;
+            return;
+        }
+
+        V_ASSERT(!p_filePath.isEmpty());
+
+        if (!VUtils::writeFileToDisk(p_filePath, p_result)) {
+            pdfPrinted = -1;
+            return;
+        }
+
+        pdfPrinted = 1;
+    }, p_layout);
+
+    while (pdfPrinted == 0) {
+        VUtils::sleepWait(100);
+
+        if (m_state == ExportState::Cancelled) {
+            break;
+        }
+    }
+
+    return pdfPrinted == 1;
+*/
+    return false;
+}
+
+bool VExporter::exportToPDFViaWK(VDocument *p_webDocument,
+                                 const ExportPDFOption &p_opt,
+                                 const QString &p_filePath,
+                                 QString *p_errMsg)
+{
+    int pdfExported = 0;
+
+    connect(p_webDocument, &VDocument::htmlContentFinished,
+            this, [&, this](const QString &p_headContent,
+                            const QString &p_styleContent,
+                            const QString &p_bodyContent) {
+                if (p_bodyContent.isEmpty() || this->m_state == ExportState::Cancelled) {
+                    pdfExported = -1;
+                    return;
+                }
+
+                Q_ASSERT(!p_filePath.isEmpty());
+
+                // Save HTML to a temp dir.
+                QTemporaryDir tmpDir;
+                if (!tmpDir.isValid()) {
+                    pdfExported = -1;
+                    return;
+                }
+
+                QDir dir(tmpDir.path());
+                QString htmlPath = dir.filePath("vnote_tmp.html");
+                QString title = p_webDocument->getFile()->getName();
+                title = QFileInfo(title).completeBaseName();
+                if (!outputToHTMLFile(htmlPath,
+                                      title,
+                                      p_headContent,
+                                      p_styleContent,
+                                      p_bodyContent,
+                                      true,
+                                      true,
+                                      false)) {
+                    pdfExported = -1;
+                    return;
+                }
+
+                // Convert via wkhtmltopdf.
+                QList<QString> files;
+                files.append(htmlPath);
+                if (!htmlsToPDFViaWK(files, p_filePath, p_opt, p_errMsg)) {
+                    pdfExported = -1;
+                } else {
+                    pdfExported = 1;
+                }
+            });
+
+    p_webDocument->getHtmlContentAsync();
+
+    while (pdfExported == 0) {
+        VUtils::sleepWait(100);
+
+        if (m_state == ExportState::Cancelled) {
+            break;
+        }
+    }
+
+    return pdfExported == 1;
+}
+
+bool VExporter::exportToCustom(VDocument *p_webDocument,
+                               const ExportCustomOption &p_opt,
+                               const QString &p_filePath,
+                               QString *p_errMsg)
+{
+    int exported = 0;
+
+    connect(p_webDocument, &VDocument::htmlContentFinished,
+            this, [&, this](const QString &p_headContent,
+                            const QString &p_styleContent,
+                            const QString &p_bodyContent) {
+                if (p_bodyContent.isEmpty() || this->m_state == ExportState::Cancelled) {
+                    exported = -1;
+                    return;
+                }
+
+                Q_ASSERT(!p_filePath.isEmpty());
+
+                // Save HTML to a temp dir.
+                QTemporaryDir tmpDir;
+                if (!tmpDir.isValid()) {
+                    exported = -1;
+                    return;
+                }
+
+                QDir dir(tmpDir.path());
+                QString htmlPath = dir.filePath("vnote_tmp.html");
+                QString title = p_webDocument->getFile()->getName();
+                title = QFileInfo(title).completeBaseName();
+                if (!outputToHTMLFile(htmlPath,
+                                      title,
+                                      p_headContent,
+                                      p_styleContent,
+                                      p_bodyContent,
+                                      true,
+                                      true,
+                                      false)) {
+                    exported = -1;
+                    return;
+                }
+
+                // Convert via custom command.
+                QList<QString> files;
+                files.append(htmlPath);
+                if (!convertFilesViaCustom(files, p_filePath, p_opt, p_errMsg)) {
+                    exported = -1;
+                } else {
+                    exported = 1;
+                }
+            });
+
+    p_webDocument->getHtmlContentAsync();
+
+    while (exported == 0) {
+        VUtils::sleepWait(100);
+
+        if (m_state == ExportState::Cancelled) {
+            break;
+        }
+    }
+
+    return exported == 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;
+        }
+
+        if (m_askedToStop) {
+            m_state = ExportState::Cancelled;
+            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:
+        V_FALLTHROUGH;
+    case ExportFormat::OnePDF:
+        if (p_opt.m_pdfOpt.m_wkhtmltopdf) {
+            exportRet = exportToPDFViaWK(m_webDocument,
+                                         p_opt.m_pdfOpt,
+                                         p_outputFile,
+                                         p_errMsg);
+        } else {
+            exportRet = exportToPDF(m_webViewer,
+                                    p_outputFile,
+                                    m_pageLayout);
+        }
+
+        break;
+
+    case ExportFormat::HTML:
+        if (p_opt.m_htmlOpt.m_mimeHTML) {
+            exportRet = exportToMHTML(m_webViewer,
+                                      p_opt.m_htmlOpt,
+                                      p_outputFile);
+        } else {
+            exportRet = exportToHTML(m_webDocument,
+                                     p_opt.m_htmlOpt,
+                                     p_outputFile);
+        }
+
+        break;
+
+    case ExportFormat::Custom:
+        exportRet = exportToCustom(m_webDocument,
+                                   p_opt.m_customOpt,
+                                   p_outputFile,
+                                   p_errMsg);
+        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(VDocument *p_webDocument,
+                             const ExportHTMLOption &p_opt,
+                             const QString &p_filePath)
+{
+    int htmlExported = 0;
+
+    connect(p_webDocument, &VDocument::htmlContentFinished,
+            this, [&, this](const QString &p_headContent,
+                            const QString &p_styleContent,
+                            const QString &p_bodyContent) {
+                if (p_bodyContent.isEmpty() || this->m_state == ExportState::Cancelled) {
+                    htmlExported = -1;
+                    return;
+                }
+
+                Q_ASSERT(!p_filePath.isEmpty());
+
+                QString title = p_webDocument->getFile()->getName();
+                title = QFileInfo(title).completeBaseName();
+                if (!outputToHTMLFile(p_filePath,
+                                      title,
+                                      p_headContent,
+                                      p_styleContent,
+                                      p_bodyContent,
+                                      p_opt.m_embedCssStyle,
+                                      p_opt.m_completeHTML,
+                                      p_opt.m_embedImages)) {
+                    htmlExported = -1;
+                    return;
+                }
+
+                htmlExported = 1;
+            });
+
+    p_webDocument->getHtmlContentAsync();
+
+    while (htmlExported == 0) {
+        VUtils::sleepWait(100);
+
+        if (m_state == ExportState::Cancelled) {
+            break;
+        }
+    }
+
+    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::embedStyleResources(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 dataURI = g_webUtils->dataURI(QUrl(reg.cap(1)), false);
+        if (dataURI.isEmpty()) {
+            pos = idx + reg.matchedLength();
+        } else {
+            // Replace the url string in html.
+            QString newUrl = QString("url('%1');").arg(dataURI);
+            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("<img ([^>]*)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("<img %1src=\"%2\"%3>").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;
+}
+
+bool VExporter::embedBodyResources(const QUrl &p_baseUrl, QString &p_html)
+{
+    bool altered = false;
+    if (p_baseUrl.isEmpty()) {
+        return altered;
+    }
+
+    QRegExp reg("<img ([^>]*)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 dataURI = g_webUtils->dataURI(srcUrl);
+        if (dataURI.isEmpty()) {
+            pos = idx + reg.matchedLength();
+        } else {
+            // Replace the url string in html.
+            QString newUrl = QString("<img %1src='%2'%3>").arg(reg.cap(1))
+                                                          .arg(dataURI)
+                                                          .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);
+}
+
+bool VExporter::exportToMHTML(VWebView *p_webViewer,
+                              const ExportHTMLOption &p_opt,
+                              const QString &p_filePath)
+{
+/*
+    Q_UNUSED(p_opt);
+
+    m_downloadState = QWebEngineDownloadItem::DownloadRequested;
+
+    p_webViewer->page()->save(p_filePath, QWebEngineDownloadItem::MimeHtmlSaveFormat);
+
+    while (m_downloadState == QWebEngineDownloadItem::DownloadRequested
+           || m_downloadState == QWebEngineDownloadItem::DownloadInProgress) {
+        VUtils::sleepWait(100);
+    }
+
+    return m_downloadState == QWebEngineDownloadItem::DownloadCompleted;
+*/
+    return false;
+}
+
+/*
+void VExporter::handleDownloadRequested(QWebEngineDownloadItem *p_item)
+{
+    if (p_item->savePageFormat() == QWebEngineDownloadItem::MimeHtmlSaveFormat) {
+        connect(p_item, &QWebEngineDownloadItem::stateChanged,
+                this, [this](QWebEngineDownloadItem::DownloadState p_state) {
+                    m_downloadState = p_state;
+                });
+    }
+}
+*/
+
+static QString combineArgs(QStringList &p_args)
+{
+    QString str;
+    for (const QString &arg : p_args) {
+        QString tmp;
+        if (arg.contains(' ')) {
+            tmp = '"' + arg + '"';
+        } else {
+            tmp = arg;
+        }
+
+        if (str.isEmpty()) {
+            str = tmp;
+        } else {
+            str = str + ' ' + tmp;
+        }
+    }
+
+    return str;
+}
+
+bool VExporter::htmlsToPDFViaWK(const QList<QString> &p_htmlFiles,
+                                const QString &p_filePath,
+                                const ExportPDFOption &p_opt,
+                                QString *p_errMsg)
+{
+    // Note: system's locale settings (Language for non-Unicode programs) is important to wkhtmltopdf.
+    // Input file could be encoded via QUrl::fromLocalFile(p_htmlFile).toString(QUrl::EncodeUnicode) to
+    // handle non-ASCII path.
+
+    QStringList args(m_wkArgs);
+
+    for (auto const & it : p_htmlFiles) {
+        args << QDir::toNativeSeparators(it);
+    }
+
+    args << QDir::toNativeSeparators(p_filePath);
+
+    QString cmd = p_opt.m_wkPath + " " + combineArgs(args);
+    emit outputLog(cmd);
+    qDebug() << "wkhtmltopdf cmd:" << cmd;
+    int ret = startProcess(p_opt.m_wkPath, args);
+    qDebug() << "wkhtmltopdf returned" << ret;
+    if (m_askedToStop) {
+        return ret == 0;
+    }
+
+    switch (ret) {
+    case -2:
+        VUtils::addErrMsg(p_errMsg, tr("Fail to start wkhtmltopdf (%1).").arg(cmd));
+        break;
+
+    case -1:
+        VUtils::addErrMsg(p_errMsg, tr("wkhtmltopdf crashed (%1).").arg(cmd));
+        break;
+
+    default:
+        break;
+    }
+
+    return ret == 0;
+}
+
+bool VExporter::convertFilesViaCustom(const QList<QString> &p_files,
+                                      const QString &p_filePath,
+                                      const ExportCustomOption &p_opt,
+                                      QString *p_errMsg)
+{
+    QString input;
+    QString inputFolder;
+    for (auto const & it : p_files) {
+        if (!input.isEmpty()) {
+            input += " ";
+        }
+
+        if (!inputFolder.isEmpty()) {
+            inputFolder += p_opt.m_folderSep;
+        }
+
+        QString tmp = QDir::toNativeSeparators(it);
+        input += ("\"" + tmp + "\"");
+        inputFolder += ("\"" + VUtils::basePathFromPath(tmp) + "\"");
+    }
+
+    QString output = QDir::toNativeSeparators(p_filePath);
+    QString cmd = evaluateCommand(p_opt,
+                                  input,
+                                  inputFolder,
+                                  output);
+    emit outputLog(cmd);
+    qDebug() << "custom cmd:" << cmd;
+    int ret = startProcess(cmd);
+    qDebug() << "custom cmd returned" << ret;
+    if (m_askedToStop) {
+        return ret == 0;
+    }
+
+    switch (ret) {
+    case -2:
+        VUtils::addErrMsg(p_errMsg, tr("Fail to start custom command (%1).").arg(cmd));
+        break;
+
+    case -1:
+        VUtils::addErrMsg(p_errMsg, tr("Custom command crashed (%1).").arg(cmd));
+        break;
+
+    default:
+        break;
+    }
+
+    return ret == 0;
+}
+
+int VExporter::exportPDFInOne(const QList<QString> &p_htmlFiles,
+                              const ExportOption &p_opt,
+                              const QString &p_outputFile,
+                              QString *p_errMsg)
+{
+    if (!htmlsToPDFViaWK(p_htmlFiles, p_outputFile, p_opt.m_pdfOpt, p_errMsg)) {
+        return 0;
+    }
+
+    return p_htmlFiles.size();
+}
+
+int VExporter::startProcess(const QString &p_program, const QStringList &p_args)
+{
+    int ret = 0;
+    QScopedPointer<QProcess> process(new QProcess(this));
+    process->start(p_program, p_args);
+    bool finished = false;
+    bool started = false;
+    while (true) {
+        QProcess::ProcessError err = process->error();
+        if (err == QProcess::FailedToStart
+            || err == QProcess::Crashed) {
+            emit outputLog(tr("QProcess error %1.").arg(err));
+            if (err == QProcess::FailedToStart) {
+                ret = -2;
+            } else {
+                ret = -1;
+            }
+
+            break;
+        }
+
+        if (started) {
+            if (process->state() == QProcess::NotRunning) {
+                finished = true;
+            }
+        } else {
+            if (process->state() != QProcess::NotRunning) {
+                started = true;
+            }
+        }
+
+        if (process->waitForFinished(500)) {
+            // Finished.
+            finished = true;
+        }
+
+        QByteArray outBa = process->readAllStandardOutput();
+        QByteArray errBa = process->readAllStandardError();
+        QString msg;
+        if (!outBa.isEmpty()) {
+            msg += QString::fromLocal8Bit(outBa);
+        }
+
+        if (!errBa.isEmpty()) {
+            msg += QString::fromLocal8Bit(errBa);
+        }
+
+        if (!msg.isEmpty()) {
+            emit outputLog(msg);
+        }
+
+        if (finished) {
+            QProcess::ExitStatus sta = process->exitStatus();
+            if (sta == QProcess::CrashExit) {
+                ret = -1;
+                break;
+            }
+
+            ret = process->exitCode();
+            break;
+        }
+
+        QCoreApplication::processEvents();
+
+        if (m_askedToStop) {
+            process->kill();
+            ret = -1;
+            break;
+        }
+    }
+
+    return ret;
+}
+
+int VExporter::startProcess(const QString &p_cmd)
+{
+    QStringList args = VUtils::parseCombinedArgString(p_cmd);
+    if (args.isEmpty()) {
+        return -2;
+    }
+
+    return startProcess(args.first(), args.mid(1));
+}
+
+bool VExporter::outputToHTMLFile(const QString &p_file,
+                                 const QString &p_title,
+                                 const QString &p_headContent,
+                                 const QString &p_styleContent,
+                                 const QString &p_bodyContent,
+                                 bool p_embedCssStyle,
+                                 bool p_completeHTML,
+                                 bool p_embedImages)
+{
+    QFile file(p_file);
+    if (!file.open(QFile::WriteOnly)) {
+        return false;
+    }
+
+    QString resFolder = QFileInfo(p_file).completeBaseName() + "_files";
+    QString resFolderPath = QDir(VUtils::basePathFromPath(p_file)).filePath(resFolder);
+
+    qDebug() << "HTML files folder" << resFolderPath;
+
+    QString html(m_exportHtmlTemplate);
+    if (!p_title.isEmpty()) {
+        html.replace(HtmlHolder::c_headTitleHolder,
+                     "<title>" + VUtils::escapeHtml(p_title) + "</title>");
+    }
+
+    if (!p_styleContent.isEmpty() && p_embedCssStyle) {
+        QString content(p_styleContent);
+        embedStyleResources(content);
+        html.replace(HtmlHolder::c_styleHolder, content);
+    }
+
+    if (!p_headContent.isEmpty()) {
+        html.replace(HtmlHolder::c_headHolder, p_headContent);
+    }
+
+    if (p_completeHTML) {
+        QString content(p_bodyContent);
+        if (p_embedImages) {
+            embedBodyResources(m_baseUrl, content);
+        } else {
+            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);
+    dir.cdUp();
+    dir.rmdir(resFolder);
+
+    return true;
+}
+
+int VExporter::exportCustomInOne(const QList<QString> &p_files,
+                                 const ExportOption &p_opt,
+                                 const QString &p_outputFile,
+                                 QString *p_errMsg)
+{
+    if (!convertFilesViaCustom(p_files, p_outputFile, p_opt.m_customOpt, p_errMsg)) {
+        return 0;
+    }
+
+    return p_files.size();
+}

+ 213 - 0
src/vexporter.h

@@ -0,0 +1,213 @@
+#ifndef VEXPORTER_H
+#define VEXPORTER_H
+
+#include <QObject>
+#include <QPageLayout>
+#include <QUrl>
+// #include <QWebEngineDownloadItem>
+#include <QStringList>
+
+#include "dialog/vexportdialog.h"
+
+class QWidget;
+class VWebView;
+class VDocument;
+
+class VExporter : public QObject
+{
+    Q_OBJECT
+public:
+    explicit VExporter(QWidget *p_parent = nullptr);
+
+    void prepareExport(const ExportOption &p_opt);
+
+    bool exportPDF(VFile *p_file,
+                   const ExportOption &p_opt,
+                   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);
+
+    bool exportCustom(VFile *p_file,
+                      const ExportOption &p_opt,
+                      const QString &p_outputFile,
+                      QString *p_errMsg = NULL);
+
+    int exportPDFInOne(const QList<QString> &p_htmlFiles,
+                       const ExportOption &p_opt,
+                       const QString &p_outputFile,
+                       QString *p_errMsg = NULL);
+
+    int exportCustomInOne(const QList<QString> &p_files,
+                          const ExportOption &p_opt,
+                          const QString &p_outputFile,
+                          QString *p_errMsg = NULL);
+
+    void setAskedToStop(bool p_askedToStop);
+
+signals:
+    // Request to output log.
+    void outputLog(const QString &p_log);
+
+private slots:
+    void handleLogicsFinished();
+
+    void handleLoadFinished(bool p_ok);
+
+    // void handleDownloadRequested(QWebEngineDownloadItem *p_item);
+
+private:
+    enum class ExportState
+    {
+        Idle = 0,
+        Cancelled,
+        Busy,
+        Failed,
+        Successful
+    };
+
+
+    enum NoteState
+    {
+        NotReady = 0,
+        WebLogicsReady = 0x1,
+        WebLoadFinished = 0x2,
+        Ready = 0x3,
+        Failed = 0x4
+    };
+
+
+    void initWebViewer(VFile *p_file, const ExportOption &p_opt);
+
+    void clearWebViewer();
+
+    void clearNoteState();
+
+    bool isNoteStateReady() const;
+
+    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 exportToPDFViaWK(VDocument *p_webDocument,
+                          const ExportPDFOption &p_opt,
+                          const QString &p_filePath,
+                          QString *p_errMsg = NULL);
+
+    bool exportToCustom(VDocument *p_webDocument,
+                        const ExportCustomOption &p_opt,
+                        const QString &p_filePath,
+                        QString *p_errMsg = NULL);
+
+    bool exportToHTML(VDocument *p_webDocument,
+                      const ExportHTMLOption &p_opt,
+                      const QString &p_filePath);
+
+    bool exportToMHTML(VWebView *p_webViewer,
+                       const ExportHTMLOption &p_opt,
+                       const QString &p_filePath);
+
+    bool htmlsToPDFViaWK(const QList<QString> &p_htmlFiles,
+                         const QString &p_filePath,
+                         const ExportPDFOption &p_opt,
+                         QString *p_errMsg = NULL);
+
+    bool convertFilesViaCustom(const QList<QString> &p_files,
+                               const QString &p_filePath,
+                               const ExportCustomOption &p_opt,
+                               QString *p_errMsg = NULL);
+
+    void prepareWKArguments(const ExportPDFOption &p_opt);
+
+    int startProcess(const QString &p_program, const QStringList &p_args);
+
+    int startProcess(const QString &p_cmd);
+
+    // @p_embedImages: embed <img> as data URI.
+    bool outputToHTMLFile(const QString &p_file,
+                          const QString &p_title,
+                          const QString &p_headContent,
+                          const QString &p_styleContent,
+                          const QString &p_bodyContent,
+                          bool p_embedCssStyle,
+                          bool p_completeHTML,
+                          bool p_embedImages);
+
+    // 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 url("...") with "file" or "qrc" schema.
+    // Embed the image data in data URIs.
+    static bool embedStyleResources(QString &p_html);
+
+    // Fix @p_html's resources like <img>.
+    // 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);
+
+    // Embed @p_html's resources like <img>.
+    static bool embedBodyResources(const QUrl &p_baseUrl, QString &p_html);
+
+    static QString getResourceRelativePath(const QString &p_file);
+
+    QPageLayout m_pageLayout;
+
+    // Will be allocated and free for each conversion.
+    VWebView *m_webViewer;
+
+    VDocument *m_webDocument;
+
+    // Base URL of VWebView.
+    QUrl m_baseUrl;
+
+    QString m_htmlTemplate;
+
+    // Template to hold the export HTML result.
+    QString m_exportHtmlTemplate;
+
+    NoteState m_noteState;
+
+    ExportState m_state;
+
+    // Download state used for MIME HTML.
+    // QWebEngineDownloadItem::DownloadState m_downloadState;
+
+    // Arguments for wkhtmltopdf.
+    QStringList m_wkArgs;
+
+    bool m_askedToStop;
+};
+
+inline void VExporter::clearNoteState()
+{
+    m_noteState = NoteState::NotReady;
+}
+
+inline bool VExporter::isNoteStateReady() const
+{
+    return m_noteState == NoteState::Ready;
+}
+
+inline bool VExporter::isNoteStateFailed() const
+{
+    return m_noteState & NoteState::Failed;
+}
+
+inline void VExporter::setAskedToStop(bool p_askedToStop)
+{
+    m_askedToStop = p_askedToStop;
+}
+#endif // VEXPORTER_H

+ 9 - 2
src/vmainwindow.cpp

@@ -50,6 +50,8 @@
 #include "vlistue.h"
 #include "vtagexplorer.h"
 #include "vmdeditor.h"
+#include "vexporter.h"
+#include "dialog/vexportdialog.h"
 
 extern VConfigManager *g_config;
 
@@ -1050,7 +1052,6 @@ void VMainWindow::initFileMenu()
 
     fileMenu->addSeparator();
 
-    /*
     m_exportAct = new QAction(tr("E&xport"), this);
     m_exportAct->setToolTip(tr("Export notes"));
     VUtils::fixTextWithCaptainShortcut(m_exportAct, "Export");
@@ -1058,7 +1059,6 @@ void VMainWindow::initFileMenu()
             this, &VMainWindow::handleExportAct);
 
     fileMenu->addAction(m_exportAct);
-    */
 
     // Print.
     m_printAct = new QAction(VIconUtils::menuIcon(":/resources/icons/print.svg"),
@@ -3309,6 +3309,13 @@ void VMainWindow::updateEditReadAct(const VEditTab *p_tab)
 
 void VMainWindow::handleExportAct()
 {
+    VExportDialog dialog(m_notebookSelector->currentNotebook(),
+                         m_dirTree->currentDirectory(),
+                         m_curFile,
+                         m_cart,
+                         g_config->getMdConverterType(),
+                         this);
+    dialog.exec();
 }
 
 VNotebook *VMainWindow::getCurrentNotebook() const

+ 2 - 1
src/vmathjaxpreviewhelper.cpp

@@ -9,6 +9,7 @@
 #include "vconfigmanager.h"
 #include "websocketclientwrapper.h"
 #include "websockettransport.h"
+#include "vconstants.h"
 
 extern VConfigManager *g_config;
 
@@ -78,7 +79,7 @@ void VMathJaxPreviewHelper::doInit()
                 emit diagramPreviewResultReady(p_identifier, p_id, p_timeStamp, p_format, ba);
             });
 
-    quint16 port = 20001;
+    quint16 port = WebSocketPort::PreviewHelperPort;
     bindToChannel(port, "content", m_webDoc);
 
     // setHtml() will change focus if it is not disabled.

+ 1 - 2
src/vmdtab.cpp

@@ -29,7 +29,6 @@ extern VMainWindow *g_mainWin;
 
 extern VConfigManager *g_config;
 
-const quint16 VMdTab::c_basePort = 10999;
 QSet<quint16> VMdTab::s_usedPorts;
 
 VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea,
@@ -1684,7 +1683,7 @@ bool VMdTab::previewExpanded() const
 
 quint16 VMdTab::getNextPort()
 {
-    auto port = c_basePort;
+    quint16 port = WebSocketPort::LastSpecialPort;
     while (s_usedPorts.find(port) != s_usedPorts.end()) {
         ++port;
     }

+ 0 - 2
src/vmdtab.h

@@ -286,8 +286,6 @@ private:
 
     int m_documentID;
 
-    static const quint16 c_basePort;
-
     static QSet<quint16> s_usedPorts;
 };