From d3f9ec48eb9e4793482fb90f49ee11c025490729 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Wed, 7 Mar 2018 21:04:19 +0800 Subject: [PATCH] export: support custom export --- src/dialog/vexportdialog.cpp | 358 +++++++++++++++++++++++++++++++---- src/dialog/vexportdialog.h | 126 +++++++++++- src/resources/vnote.ini | 7 + src/vconfigmanager.h | 14 ++ src/vdirectory.cpp | 26 +++ src/vdirectory.h | 3 + src/vexporter.cpp | 327 ++++++++++++++++++++++++++------ src/vexporter.h | 29 +++ src/vnotebook.cpp | 19 ++ src/vnotebook.h | 2 + 10 files changed, 803 insertions(+), 108 deletions(-) diff --git a/src/dialog/vexportdialog.cpp b/src/dialog/vexportdialog.cpp index 116a67b0..2dd12eda 100644 --- a/src/dialog/vexportdialog.cpp +++ b/src/dialog/vexportdialog.cpp @@ -174,11 +174,13 @@ void VExportDialog::setupUI() 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); @@ -235,41 +237,41 @@ QWidget *VExportDialog::setupPDFAdvancedSettings() // wkhtmltopdf Path. m_wkPathEdit = new VLineEdit(); m_wkPathEdit->setToolTip(tr("Tell VNote where to find wkhtmltopdf tool")); - m_wkPathEdit->setEnabled(m_wkhtmltopdfCB->isChecked()); + m_wkPathEdit->setEnabled(false); m_wkPathBrowseBtn = new QPushButton(tr("&Browse")); - m_wkPathBrowseBtn->setEnabled(m_wkhtmltopdfCB->isChecked()); connect(m_wkPathBrowseBtn, &QPushButton::clicked, this, &VExportDialog::handleWkPathBrowseBtnClicked); + m_wkPathBrowseBtn->setEnabled(false); m_wkTitleEdit = new VLineEdit(); - m_wkTitleEdit->setPlaceholderText(tr("Use the name of the first source note")); + 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(m_wkhtmltopdfCB->isChecked()); + m_wkTitleEdit->setEnabled(false); m_wkTargetFileNameEdit = new VLineEdit(); - m_wkTargetFileNameEdit->setPlaceholderText(tr("Use the name of the first source note")); + 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(m_wkhtmltopdfCB->isChecked()); + 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(m_wkhtmltopdfCB->isChecked()); + m_wkBackgroundCB->setEnabled(false); // wkhtmltopdf page number. m_wkPageNumberCB = VUtils::getComboBox(); m_wkPageNumberCB->setToolTip(tr("Append page number as footer")); - m_wkPageNumberCB->setEnabled(m_wkhtmltopdfCB->isChecked()); + 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(m_wkhtmltopdfCB->isChecked()); + m_wkExtraArgsEdit->setEnabled(false); QGridLayout *advLayout = new QGridLayout(); advLayout->addWidget(new QLabel(tr("Page layout:")), 0, 0); @@ -387,6 +389,7 @@ void VExportDialog::initUIFields(MarkdownConverterType p_renderer) 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. @@ -451,6 +454,22 @@ void VExportDialog::initUIFields(MarkdownConverterType p_renderer) 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_customFolderSepEdit->setText(s_opt.m_customOpt.m_folderSep); } bool VExportDialog::checkWkhtmltopdfExecutable(const QString &p_file) @@ -493,11 +512,14 @@ void VExportDialog::startExport() QString outputFolder = QDir::cleanPath(QDir(getOutputDirectory()).absolutePath()); + QString renderStyle = m_renderStyleCB->currentData().toString(); + QString cssUrl = g_config->getCssStyleUrl(renderStyle); + s_opt = ExportOption(currentSource(), currentFormat(), (MarkdownConverterType)m_rendererCB->currentData().toInt(), m_renderBgCB->currentData().toString(), - m_renderStyleCB->currentData().toString(), + renderStyle, m_renderCodeBlockStyleCB->currentData().toString(), m_subfolderCB->isChecked(), ExportPDFOption(&m_pageLayout, @@ -507,11 +529,20 @@ void VExportDialog::startExport() m_tableOfContentsCB->isChecked(), m_wkTitleEdit->text(), m_wkTargetFileNameEdit->text(), - (ExportPageNumber)m_wkPageNumberCB->currentData().toInt(), + (ExportPageNumber) + m_wkPageNumberCB->currentData().toInt(), m_wkExtraArgsEdit->text()), ExportHTMLOption(m_embedStyleCB->isChecked(), m_completeHTMLCB->isChecked(), - m_mimeHTMLCB->isChecked())); + m_mimeHTMLCB->isChecked()), + ExportCustomOption((ExportCustomOption::SourceFormat) + m_customSrcFormatCB->currentData().toInt(), + m_customSuffixEdit->text(), + m_customCmdEdit->toPlainText(), + cssUrl, + m_customAllInOneCB->isChecked(), + m_customFolderSepEdit->text(), + m_customTargetFileNameEdit->text())); m_consoleEdit->clear(); appendLogLine(tr("Export to %1.").arg(outputFolder)); @@ -521,7 +552,6 @@ void VExportDialog::startExport() || s_opt.m_format == ExportFormat::HTML) { if (s_opt.m_format != ExportFormat::OnePDF) { s_opt.m_pdfOpt.m_wkTitle.clear(); - s_opt.m_pdfOpt.m_wkTargetFileName.clear(); } if ((s_opt.m_format == ExportFormat::PDF @@ -539,6 +569,24 @@ void VExportDialog::startExport() } 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; @@ -546,39 +594,13 @@ void VExportDialog::startExport() if (s_opt.m_format == ExportFormat::OnePDF) { QList files; - // Output HTMLs to a tmp folder. QTemporaryDir tmpDir; if (!tmpDir.isValid()) { goto exit; } - qDebug() << "output HTMLs to temporary dir" << tmpDir.path(); - - s_opt.m_format = ExportFormat::HTML; - switch (s_opt.m_source) { - case ExportSource::CurrentNote: - ret = doExport(m_file, s_opt, tmpDir.path(), &msg, &files); - break; - - case ExportSource::CurrentFolder: - ret = doExport(m_directory, s_opt, tmpDir.path(), &msg, &files); - break; - - case ExportSource::CurrentNotebook: - ret = doExport(m_notebook, s_opt, tmpDir.path(), &msg, &files); - break; - - case ExportSource::Cart: - ret = doExport(m_cart, s_opt, tmpDir.path(), &msg, &files); - break; - - default: - break; - } - - s_opt.m_format = ExportFormat::OnePDF; - + ret = outputAsHTML(tmpDir.path(), &msg, &files); if (m_askedToStop) { ret = 0; goto exit; @@ -588,6 +610,31 @@ void VExportDialog::startExport() 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 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: @@ -735,6 +782,10 @@ int VExportDialog::doExport(VFile *p_file, 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; } @@ -1011,6 +1062,45 @@ int VExportDialog::doExportHTML(VFile *p_file, } } +int VExportDialog::doExportCustom(VFile *p_file, + const ExportOption &p_opt, + const QString &p_outputFolder, + QString *p_errMsg, + QList *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) { @@ -1054,6 +1144,7 @@ void VExportDialog::handleCurrentFormatChanged(int p_index) bool pdfEnabled = false; bool htmlEnabled = false; bool pdfTitleNameEnabled = false; + bool customEnabled = false; if (p_index >= 0) { switch (currentFormat()) { @@ -1073,6 +1164,10 @@ void VExportDialog::handleCurrentFormatChanged(int p_index) m_wkhtmltopdfCB->setEnabled(false); break; + case ExportFormat::Custom: + customEnabled = true; + break; + default: break; } @@ -1080,6 +1175,7 @@ void VExportDialog::handleCurrentFormatChanged(int p_index) m_pdfSettings->setVisible(pdfEnabled); m_htmlSettings->setVisible(htmlEnabled); + m_customSettings->setVisible(customEnabled); m_wkTitleEdit->setEnabled(pdfTitleNameEnabled); m_wkTargetFileNameEdit->setEnabled(pdfTitleNameEnabled); @@ -1129,8 +1225,6 @@ int VExportDialog::doExportPDFAllInOne(const QList &p_files, QString outputPath = QDir(p_outputFolder).filePath(name); - qDebug() << "output" << p_files.size() << "HTML files as PDF to" << outputPath; - 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)); @@ -1140,3 +1234,183 @@ int VExportDialog::doExportPDFAllInOne(const QList &p_files, return ret; } + +int VExportDialog::doExportCustomAllInOne(const QList &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(); + m_customSrcFormatCB->setToolTip(tr("Choose format of the input")); + + // Output suffix. + m_customSuffixEdit = new VLineEdit(); + 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("%0 for the input file; " + "%1 for the output file; " + "%2 for the rendering CSS style file; " + "%3 for the input file directory.")); + tipsLabel->setWordWrap(true); + + // Enable All In One. + m_customAllInOneCB = new QCheckBox(tr("Enable All In One")); + 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); + }); + + // Input directory separator. + m_customFolderSepEdit = new VLineEdit(); + 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(); + 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(); + m_customCmdEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + QString cmdExamp("pandoc --resource-path=.:\"%3\" --css=\"%2\" -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(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(100); + + return wid; +} + +int VExportDialog::outputAsHTML(QString &p_outputFolder, + QString *p_errMsg, + QList *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 VExportDialog::collectFiles(QString *p_errMsg) +{ + Q_UNUSED(p_errMsg); + + QList 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; +} diff --git a/src/dialog/vexportdialog.h b/src/dialog/vexportdialog.h index 64c88000..7ebd3718 100644 --- a/src/dialog/vexportdialog.h +++ b/src/dialog/vexportdialog.h @@ -38,7 +38,8 @@ enum class ExportFormat Markdown = 0, HTML, PDF, - OnePDF + OnePDF, + Custom }; @@ -119,6 +120,87 @@ struct ExportPDFOption }; +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_folderSep(DEFAULT_SEP) + { + } + + ExportCustomOption(const QStringList &p_config) + : m_srcFormat(SourceFormat::Markdown), + m_allInOne(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, + bool p_allInOne, + const QString &p_folderSep, + const QString &p_targetFileName) + : m_srcFormat(p_srcFormat), + m_outputSuffix(p_outputSuffix), + m_cssUrl(p_cssUrl), + m_allInOne(p_allInOne), + 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; + bool m_allInOne; + + QString m_folderSep; + QString m_targetFileName; +}; + + struct ExportOption { ExportOption() @@ -137,7 +219,8 @@ struct ExportOption const QString &p_renderCodeBlockStyle, bool p_processSubfolders, const ExportPDFOption &p_pdfOpt, - const ExportHTMLOption &p_htmlOpt) + const ExportHTMLOption &p_htmlOpt, + const ExportCustomOption &p_customOpt) : m_source(p_source), m_format(p_format), m_renderer(p_renderer), @@ -146,7 +229,8 @@ struct ExportOption m_renderCodeBlockStyle(p_renderCodeBlockStyle), m_processSubfolders(p_processSubfolders), m_pdfOpt(p_pdfOpt), - m_htmlOpt(p_htmlOpt) + m_htmlOpt(p_htmlOpt), + m_customOpt(p_customOpt) { } @@ -166,6 +250,8 @@ struct ExportOption ExportPDFOption m_pdfOpt; ExportHTMLOption m_htmlOpt; + + ExportCustomOption m_customOpt; }; @@ -204,6 +290,8 @@ private: QWidget *setupGeneralAdvancedSettings(); + QWidget *setupCustomAdvancedSettings(); + void initUIFields(MarkdownConverterType p_renderer); QString getOutputDirectory() const; @@ -258,6 +346,17 @@ private: const QString &p_outputFolder, QString *p_errMsg = NULL); + int doExportCustomAllInOne(const QList &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 *p_outputFiles = NULL); + // Return false if we could not continue. bool checkUserAction(); @@ -269,6 +368,13 @@ private: ExportFormat currentFormat() const; + int outputAsHTML(QString &p_outputFolder, + QString *p_errMsg = NULL, + QList *p_outputFiles = NULL); + + // Collect files to be handled. + QList collectFiles(QString *p_errMsg = NULL); + QComboBox *m_srcCB; QComboBox *m_formatCB; @@ -293,6 +399,8 @@ private: QWidget *m_generalSettings; + QWidget *m_customSettings; + QPlainTextEdit *m_consoleEdit; QDialogButtonBox *m_btnBox; @@ -329,6 +437,18 @@ private: QCheckBox *m_subfolderCB; + QComboBox *m_customSrcFormatCB; + + VLineEdit *m_customSuffixEdit; + + QCheckBox *m_customAllInOneCB; + + QPlainTextEdit *m_customCmdEdit; + + VLineEdit *m_customFolderSepEdit; + + VLineEdit *m_customTargetFileNameEdit; + VNotebook *m_notebook; VDirectory *m_directory; diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index 947e296b..044bb6e8 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -209,6 +209,13 @@ wkhtmltopdf=wkhtmltopdf ; Double quotes to enclose arguments with spaces wkhtmltopdfArgs= +; A string list separated by , +; SourceFormat,OutputSuffix,CMD +; SourceFormat: 0 for Markdown, 1 for HTML +; OutputSuffix: suffix WITHOUT the preceding dot +; CMD: command to execute, %0 for the input file, %1 for the output file +custom_export= + [web] ; Location and configuration for Mathjax mathjax_javascript=https://cdn.bootcss.com/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_HTMLorMML diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 24aaa023..d99f7c90 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -463,6 +463,9 @@ public: bool getEnableFlashAnchor() const; void setEnableFlashAnchor(bool p_enabled); + QStringList getCustomExport() const; + void setCustomExport(const QStringList &p_exp); + private: // Look up a config from user and default settings. QVariant getConfigFromSettings(const QString §ion, const QString &key) const; @@ -2132,4 +2135,15 @@ inline void VConfigManager::setEnableFlashAnchor(bool p_enabled) m_enableFlashAnchor = p_enabled; setConfigToSettings("web", "enable_flash_anchor", m_enableFlashAnchor); } + +inline QStringList VConfigManager::getCustomExport() const +{ + return getConfigFromSettings("export", + "custom_export").toStringList(); +} + +inline void VConfigManager::setCustomExport(const QStringList &p_exp) +{ + setConfigToSettings("export", "custom_export", p_exp); +} #endif // VCONFIGMANAGER_H diff --git a/src/vdirectory.cpp b/src/vdirectory.cpp index 563f06a1..fcfc159b 100644 --- a/src/vdirectory.cpp +++ b/src/vdirectory.cpp @@ -716,3 +716,29 @@ bool VDirectory::sortSubDirectories(const QVector &p_sortedIdx) return ret; } + +QList VDirectory::collectFiles() +{ + QList files; + bool opened = isOpened(); + if (!opened && !open()) { + qWarning() << "fail to open directory" << fetchPath(); + return files; + } + + // Files. + for (auto const & file : m_files) { + files.append(file->fetchPath()); + } + + // Subfolders. + for (auto const & dir : m_subDirs) { + files.append(dir->collectFiles()); + } + + if (!opened) { + close(); + } + + return files; +} diff --git a/src/vdirectory.h b/src/vdirectory.h index 6d1983e3..5c431540 100644 --- a/src/vdirectory.h +++ b/src/vdirectory.h @@ -114,6 +114,9 @@ public: // Reorder sub-directories in m_subDirs by index. bool sortSubDirectories(const QVector &p_sortedIdx); + // Return path of files in this directory recursively. + QList collectFiles(); + // Delete directory @p_dir. static bool deleteDirectory(VDirectory *p_dir, bool p_skipRecycleBin = false, diff --git a/src/vexporter.cpp b/src/vexporter.cpp index 6f683902..fcb1af73 100644 --- a/src/vexporter.cpp +++ b/src/vexporter.cpp @@ -40,7 +40,8 @@ static QString marginToStrMM(qreal 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::OnePDF + || p_opt.m_format == ExportFormat::Custom; bool extraToc = isPdf && !p_opt.m_pdfOpt.m_wkhtmltopdf && p_opt.m_pdfOpt.m_enableTableOfContents; @@ -178,6 +179,69 @@ bool VExporter::exportHTML(VFile *p_file, 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 cssStyle = QDir::toNativeSeparators(p_opt.m_cssUrl); + + QString cmd(p_opt.m_cmd); + replaceArgument(cmd, "%0", p_input); + replaceArgument(cmd, "%1", p_output); + replaceArgument(cmd, "%2", cssStyle); + replaceArgument(cmd, "%3", p_inputFolder); + + 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 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); @@ -308,36 +372,16 @@ bool VExporter::exportToPDFViaWK(VDocument *p_webDocument, } QString htmlPath = tmpDir.filePath("vnote_tmp.html"); - - QFile file(htmlPath); - if (!file.open(QFile::WriteOnly)) { + if (!outputToHTMLFile(htmlPath, + p_headContent, + p_styleContent, + p_bodyContent, + true, + true)) { pdfExported = -1; return; } - QString resFolder = QFileInfo(htmlPath).completeBaseName() + "_files"; - QString resFolderPath = QDir(VUtils::basePathFromPath(htmlPath)).filePath(resFolder); - - qDebug() << "temp HTML files folder" << resFolderPath; - - QString html(m_exportHtmlTemplate); - if (!p_styleContent.isEmpty()) { - QString content(p_styleContent); - fixStyleResources(resFolderPath, content); - html.replace(HtmlHolder::c_styleHolder, content); - } - - if (!p_headContent.isEmpty()) { - html.replace(HtmlHolder::c_headHolder, p_headContent); - } - - QString content(p_bodyContent); - fixBodyResources(m_baseUrl, resFolderPath, content); - html.replace(HtmlHolder::c_bodyHolder, content); - - file.write(html.toUtf8()); - file.close(); - // Convert via wkhtmltopdf. QList files; files.append(htmlPath); @@ -361,6 +405,65 @@ bool VExporter::exportToPDFViaWK(VDocument *p_webDocument, 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; + } + + QString htmlPath = tmpDir.filePath("vnote_tmp.html"); + if (!outputToHTMLFile(htmlPath, + p_headContent, + p_styleContent, + p_bodyContent, + true, + true)) { + exported = -1; + return; + } + + // Convert via custom command. + QList 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, @@ -440,6 +543,13 @@ bool VExporter::exportViaWebView(VFile *p_file, break; + case ExportFormat::Custom: + exportRet = exportToCustom(m_webDocument, + p_opt.m_customOpt, + p_outputFile, + p_errMsg); + break; + default: break; } @@ -487,46 +597,16 @@ bool VExporter::exportToHTML(VDocument *p_webDocument, Q_ASSERT(!p_filePath.isEmpty()); - QFile file(p_filePath); - if (!file.open(QFile::WriteOnly)) { + if (!outputToHTMLFile(p_filePath, + p_headContent, + p_styleContent, + p_bodyContent, + p_opt.m_embedCssStyle, + p_opt.m_completeHTML)) { htmlExported = -1; return; } - QString resFolder = QFileInfo(p_filePath).completeBaseName() + "_files"; - QString resFolderPath = QDir(VUtils::basePathFromPath(p_filePath)).filePath(resFolder); - - qDebug() << "HTML files folder" << resFolderPath; - - QString html(m_exportHtmlTemplate); - if (!p_styleContent.isEmpty() && p_opt.m_embedCssStyle) { - QString content(p_styleContent); - fixStyleResources(resFolderPath, content); - html.replace(HtmlHolder::c_styleHolder, content); - } - - if (!p_headContent.isEmpty()) { - html.replace(HtmlHolder::c_headHolder, p_headContent); - } - - if (p_opt.m_completeHTML) { - QString content(p_bodyContent); - fixBodyResources(m_baseUrl, resFolderPath, content); - html.replace(HtmlHolder::c_bodyHolder, content); - } else { - html.replace(HtmlHolder::c_bodyHolder, p_bodyContent); - } - - file.write(html.toUtf8()); - file.close(); - - // Delete empty resource folder. - QDir dir(resFolderPath); - if (dir.isEmpty()) { - dir.cdUp(); - dir.rmdir(resFolder); - } - htmlExported = 1; }); @@ -711,6 +791,56 @@ bool VExporter::htmlsToPDFViaWK(const QList &p_htmlFiles, return ret == 0; } +bool VExporter::convertFilesViaCustom(const QList &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 &p_htmlFiles, const ExportOption &p_opt, const QString &p_outputFile, @@ -796,3 +926,74 @@ int VExporter::startProcess(const QString &p_program, const QStringList &p_args) return ret; } + +int VExporter::startProcess(const QString &p_cmd) +{ + QStringList args = 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_headContent, + const QString &p_styleContent, + const QString &p_bodyContent, + bool p_embedCssStyle, + bool p_completeHTML) +{ + 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_styleContent.isEmpty() && p_embedCssStyle) { + QString content(p_styleContent); + fixStyleResources(resFolderPath, 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); + fixBodyResources(m_baseUrl, resFolderPath, content); + html.replace(HtmlHolder::c_bodyHolder, content); + } else { + html.replace(HtmlHolder::c_bodyHolder, p_bodyContent); + } + + file.write(html.toUtf8()); + file.close(); + + // Delete empty resource folder. + QDir dir(resFolderPath); + if (dir.isEmpty()) { + dir.cdUp(); + dir.rmdir(resFolder); + } + + return true; +} + +int VExporter::exportCustomInOne(const QList &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(); +} diff --git a/src/vexporter.h b/src/vexporter.h index 6a2be69a..0afa9a76 100644 --- a/src/vexporter.h +++ b/src/vexporter.h @@ -31,11 +31,21 @@ public: 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 &p_htmlFiles, const ExportOption &p_opt, const QString &p_outputFile, QString *p_errMsg = NULL); + int exportCustomInOne(const QList &p_files, + const ExportOption &p_opt, + const QString &p_outputFile, + QString *p_errMsg = NULL); + void setAskedToStop(bool p_askedToStop); signals: @@ -94,6 +104,11 @@ private: 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); @@ -107,10 +122,24 @@ private: const ExportPDFOption &p_opt, QString *p_errMsg = NULL); + bool convertFilesViaCustom(const QList &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); + + bool outputToHTMLFile(const QString &p_file, + const QString &p_headContent, + const QString &p_styleContent, + const QString &p_bodyContent, + bool p_embedCssStyle, + bool p_completeHTML); + // 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, diff --git a/src/vnotebook.cpp b/src/vnotebook.cpp index a4ecb3ae..ee7fef05 100644 --- a/src/vnotebook.cpp +++ b/src/vnotebook.cpp @@ -367,3 +367,22 @@ QString VNotebook::getRecycleBinFolderPath() const return QDir(m_path).filePath(m_recycleBinFolder); } } + +QList VNotebook::collectFiles() +{ + QList files; + + bool opened = isOpened(); + if (!opened && !open()) { + qWarning() << "fail to open notebook %1" << m_path; + return files; + } + + files = m_rootDir->collectFiles(); + + if (!opened) { + close(); + } + + return files; +} diff --git a/src/vnotebook.h b/src/vnotebook.h index 316ca109..71cc6533 100644 --- a/src/vnotebook.h +++ b/src/vnotebook.h @@ -95,6 +95,8 @@ public: bool isValid() const; + QList collectFiles(); + private: // Serialize current instance to json. QJsonObject toConfigJson() const;