support exporting note as PDF file

TODO: Currently the exported PDF does not have the outline which is
needed to fix via third-party utils.
This commit is contained in:
Le Tan 2017-05-21 09:34:32 +08:00
parent 871c53743f
commit 0131569c02
26 changed files with 696 additions and 48 deletions

View File

@ -94,7 +94,7 @@ void VInsertImageDialog::handleBrowseBtnClicked()
static QString lastPath = QDir::homePath(); static QString lastPath = QDir::homePath();
QString filePath = QFileDialog::getOpenFileName(this, tr("Select The Image To Be Inserted"), QString filePath = QFileDialog::getOpenFileName(this, tr("Select The Image To Be Inserted"),
lastPath, tr("Images (*.png *.xpm *.jpg *.bmp *.gif)")); lastPath, tr("Images (*.png *.xpm *.jpg *.bmp *.gif)"));
if (filePath.isNull() || filePath.isEmpty()) { if (filePath.isEmpty()) {
return; return;
} }

View File

@ -49,13 +49,18 @@ var updateHtml = function(html) {
} }
} }
// If you add new logics after handling MathJax, please pay attention to
// finishLoading logic.
// MathJax may be not loaded for now. // MathJax may be not loaded for now.
if (VEnableMathjax && (typeof MathJax != "undefined")) { if (VEnableMathjax && (typeof MathJax != "undefined")) {
try { try {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder]); MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, finishLoading]);
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
finishLoading();
} }
} else {
finishLoading();
} }
}; };

View File

@ -169,12 +169,18 @@ var updateText = function(text) {
handleToc(needToc); handleToc(needToc);
insertImageCaption(); insertImageCaption();
renderMermaid('lang-mermaid'); renderMermaid('lang-mermaid');
// If you add new logics after handling MathJax, please pay attention to
// finishLoading logic.
if (VEnableMathjax) { if (VEnableMathjax) {
try { try {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder]); MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, finishLoading]);
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
finishLoading();
} }
} else {
finishLoading();
} }
}; };

View File

@ -245,3 +245,9 @@ var insertImageCaption = function() {
img.insertAdjacentElement('afterend', captionDiv); img.insertAdjacentElement('afterend', captionDiv);
} }
} }
// The renderer specific code should call this function once thay have finished
// loading the page.
var finishLoading = function() {
content.finishLoading();
};

View File

@ -127,12 +127,18 @@ var updateText = function(text) {
handleToc(needToc); handleToc(needToc);
insertImageCaption(); insertImageCaption();
renderMermaid('lang-mermaid'); renderMermaid('lang-mermaid');
// If you add new logics after handling MathJax, please pay attention to
// finishLoading logic.
if (VEnableMathjax) { if (VEnableMathjax) {
try { try {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder]); MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, finishLoading]);
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
finishLoading();
} }
} else {
finishLoading();
} }
}; };

View File

@ -151,12 +151,18 @@ var updateText = function(text) {
insertImageCaption(); insertImageCaption();
highlightCodeBlocks(document, VEnableMermaid); highlightCodeBlocks(document, VEnableMermaid);
renderMermaid('language-mermaid'); renderMermaid('language-mermaid');
// If you add new logics after handling MathJax, please pay attention to
// finishLoading logic.
if (VEnableMathjax) { if (VEnableMathjax) {
try { try {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder]); MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, finishLoading]);
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
finishLoading();
} }
} else {
finishLoading();
} }
}; };

View File

@ -61,7 +61,8 @@ SOURCES += main.cpp\
vorphanfile.cpp \ vorphanfile.cpp \
vcodeblockhighlighthelper.cpp \ vcodeblockhighlighthelper.cpp \
vwebview.cpp \ vwebview.cpp \
vimagepreviewer.cpp vimagepreviewer.cpp \
vexporter.cpp
HEADERS += vmainwindow.h \ HEADERS += vmainwindow.h \
vdirectorytree.h \ vdirectorytree.h \
@ -109,7 +110,8 @@ HEADERS += vmainwindow.h \
vorphanfile.h \ vorphanfile.h \
vcodeblockhighlighthelper.h \ vcodeblockhighlighthelper.h \
vwebview.h \ vwebview.h \
vimagepreviewer.h vimagepreviewer.h \
vexporter.h
RESOURCES += \ RESOURCES += \
vnote.qrc \ vnote.qrc \

View File

@ -16,6 +16,7 @@
#include <cmath> #include <cmath>
#include <QLocale> #include <QLocale>
#include <QPushButton> #include <QPushButton>
#include <QElapsedTimer>
#include "vfile.h" #include "vfile.h"
@ -441,3 +442,16 @@ QString VUtils::getLocale()
} }
return locale; return locale;
} }
void VUtils::sleepWait(int p_milliseconds)
{
if (p_milliseconds <= 0) {
return;
}
QElapsedTimer t;
t.start();
while (t.elapsed() < p_milliseconds) {
QCoreApplication::processEvents();
}
}

View File

@ -92,6 +92,8 @@ public:
static QChar keyToChar(int p_key); static QChar keyToChar(int p_key);
static QString getLocale(); static QString getLocale();
static void sleepWait(int p_milliseconds);
// Regular expression for image link. // Regular expression for image link.
// ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" ) // ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" )
// Captured texts (need to be trimmed): // Captured texts (need to be trimmed):

View File

@ -14,4 +14,8 @@ static const qreal c_webZoomFactorMax = 5;
static const qreal c_webZoomFactorMin = 0.25; static const qreal c_webZoomFactorMin = 0.25;
static const int c_tabSequenceBase = 1; static const int c_tabSequenceBase = 1;
// HTML and JS.
static const QString c_htmlJSHolder = "JS_PLACE_HOLDER";
static const QString c_htmlExtraHolder = "<!-- EXTRA_PLACE_HOLDER -->";
#endif #endif

View File

@ -9,7 +9,9 @@ VDocument::VDocument(const VFile *v_file, QObject *p_parent)
void VDocument::updateText() void VDocument::updateText()
{ {
if (m_file) {
emit textChanged(m_file->getContent()); emit textChanged(m_file->getContent());
}
} }
void VDocument::setToc(const QString &toc) void VDocument::setToc(const QString &toc)
@ -74,3 +76,14 @@ void VDocument::noticeReadyToHighlightText()
{ {
emit readyToHighlightText(); emit readyToHighlightText();
} }
void VDocument::setFile(const VFile *p_file)
{
m_file = p_file;
}
void VDocument::finishLoading()
{
qDebug() << "Web side finished loading";
emit loadFinished();
}

View File

@ -14,6 +14,7 @@ class VDocument : public QObject
Q_PROPERTY(QString html MEMBER m_html NOTIFY htmlChanged) Q_PROPERTY(QString html MEMBER m_html NOTIFY htmlChanged)
public: public:
// @p_file could be NULL.
VDocument(const VFile *p_file, QObject *p_parent = 0); VDocument(const VFile *p_file, QObject *p_parent = 0);
QString getToc(); QString getToc();
void scrollToAnchor(const QString &anchor); void scrollToAnchor(const QString &anchor);
@ -22,6 +23,8 @@ public:
// Use p_id to identify the result. // Use p_id to identify the result.
void highlightTextAsync(const QString &p_text, int p_id, int p_timeStamp); void highlightTextAsync(const QString &p_text, int p_id, int p_timeStamp);
void setFile(const VFile *p_file);
public slots: public slots:
// Will be called in the HTML side // Will be called in the HTML side
void setToc(const QString &toc); void setToc(const QString &toc);
@ -32,6 +35,9 @@ public slots:
void highlightTextCB(const QString &p_html, int p_id, int p_timeStamp); void highlightTextCB(const QString &p_html, int p_id, int p_timeStamp);
void noticeReadyToHighlightText(); void noticeReadyToHighlightText();
// Page is finished loading.
void finishLoading();
signals: signals:
void textChanged(const QString &text); void textChanged(const QString &text);
void tocChanged(const QString &toc); void tocChanged(const QString &toc);
@ -43,6 +49,7 @@ signals:
void requestHighlightText(const QString &p_text, int p_id, int p_timeStamp); void requestHighlightText(const QString &p_text, int p_id, int p_timeStamp);
void textHighlighted(const QString &p_html, int p_id, int p_timeStamp); void textHighlighted(const QString &p_html, int p_id, int p_timeStamp);
void readyToHighlightText(); void readyToHighlightText();
void loadFinished();
private: private:
QString m_toc; QString m_toc;

View File

@ -155,25 +155,14 @@ void VEditTab::scrollPreviewToHeader(int p_outlineIndex)
void VEditTab::previewByConverter() void VEditTab::previewByConverter()
{ {
VMarkdownConverter mdConverter; VMarkdownConverter mdConverter;
const QString &content = m_file->getContent(); QString toc;
QString html = mdConverter.generateHtml(content, vconfig.getMarkdownExtensions()); QString html = mdConverter.generateHtml(m_file->getContent(),
QRegularExpression tocExp("<p>\\[TOC\\]<\\/p>", QRegularExpression::CaseInsensitiveOption); vconfig.getMarkdownExtensions(),
QString toc = mdConverter.generateToc(content, vconfig.getMarkdownExtensions()); toc);
processHoedownToc(toc);
html.replace(tocExp, toc);
document.setHtml(html); document.setHtml(html);
updateTocFromHtml(toc); updateTocFromHtml(toc);
} }
void VEditTab::processHoedownToc(QString &p_toc)
{
// Hoedown will add '\n'.
p_toc.replace("\n", "");
// Hoedown will translate `_` in title to `<em>`.
p_toc.replace("<em>", "_");
p_toc.replace("</em>", "_");
}
void VEditTab::showFileEditMode() void VEditTab::showFileEditMode()
{ {
if (!m_file->isModifiable()) { if (!m_file->isModifiable()) {
@ -294,8 +283,8 @@ void VEditTab::discardAndRead()
void VEditTab::setupMarkdownPreview() void VEditTab::setupMarkdownPreview()
{ {
const QString jsHolder("JS_PLACE_HOLDER"); const QString &jsHolder = c_htmlJSHolder;
const QString extraHolder("<!-- EXTRA_PLACE_HOLDER -->"); const QString &extraHolder = c_htmlExtraHolder;
webPreviewer = new VWebView(m_file, this); webPreviewer = new VWebView(m_file, this);
connect(webPreviewer, &VWebView::editNote, connect(webPreviewer, &VWebView::editNote,
@ -373,24 +362,7 @@ void VEditTab::setupMarkdownPreview()
htmlTemplate.replace(extraHolder, extraFile); htmlTemplate.replace(extraHolder, extraFile);
} }
// Need to judge the path: Url, local file, resource file. webPreviewer->setHtml(htmlTemplate, m_file->getBaseUrl());
QUrl baseUrl;
QString basePath = m_file->retriveBasePath();
QFileInfo pathInfo(basePath);
if (pathInfo.exists()) {
if (pathInfo.isNativePath()) {
// Local file.
baseUrl = QUrl::fromLocalFile(basePath + QDir::separator());
} else {
// Resource file.
baseUrl = QUrl("qrc" + basePath + QDir::separator());
}
} else {
// Url.
baseUrl = QUrl(basePath + QDir::separator());
}
webPreviewer->setHtml(htmlTemplate, baseUrl);
addWidget(webPreviewer); addWidget(webPreviewer);
} }
@ -733,3 +705,7 @@ VWebView *VEditTab::getWebViewer() const
return webPreviewer; return webPreviewer;
} }
MarkdownConverterType VEditTab::getMarkdownConverterType() const
{
return mdConverterType;
}

View File

@ -51,6 +51,8 @@ public:
VWebView *getWebViewer() const; VWebView *getWebViewer() const;
MarkdownConverterType getMarkdownConverterType() const;
public slots: public slots:
// Enter edit mode // Enter edit mode
void editFile(); void editFile();
@ -82,7 +84,6 @@ private:
void showFileEditMode(); void showFileEditMode();
void setupMarkdownPreview(); void setupMarkdownPreview();
void previewByConverter(); void previewByConverter();
void processHoedownToc(QString &p_toc);
inline bool isChild(QObject *obj); inline bool isChild(QObject *obj);
void parseTocUl(QXmlStreamReader &xml, QVector<VHeader> &headers, int level); void parseTocUl(QXmlStreamReader &xml, QVector<VHeader> &headers, int level);
void parseTocLi(QXmlStreamReader &xml, QVector<VHeader> &headers, int level); void parseTocLi(QXmlStreamReader &xml, QVector<VHeader> &headers, int level);

404
src/vexporter.cpp Normal file
View File

@ -0,0 +1,404 @@
#include "vexporter.h"
#include <QtWidgets>
#include <QFileInfo>
#include <QDir>
#include <QWebChannel>
#include <QDebug>
#include <QVBoxLayout>
#include <QShowEvent>
#ifndef QT_NO_PRINTER
#include <QPrinter>
#include <QPageSetupDialog>
#endif
#include "vconfigmanager.h"
#include "utils/vutils.h"
#include "vfile.h"
#include "vwebview.h"
#include "vpreviewpage.h"
#include "vconstants.h"
#include "vnote.h"
#include "vmarkdownconverter.h"
extern VConfigManager vconfig;
QString VExporter::s_defaultPathDir = QDir::homePath();
VExporter::VExporter(MarkdownConverterType p_mdType, QWidget *p_parent)
: QDialog(p_parent), m_document(NULL, this), m_mdType(p_mdType),
m_file(NULL), m_type(ExportType::PDF), m_source(ExportSource::Invalid),
m_webReady(false), m_state(ExportState::Idle),
m_pageLayout(QPageLayout(QPageSize(QPageSize::A4), QPageLayout::Portrait, QMarginsF(0.0, 0.0, 0.0, 0.0)))
{
setupUI();
}
void VExporter::setupUI()
{
setupMarkdownViewer();
m_infoLabel = new QLabel();
m_infoLabel->setWordWrap(true);
// Target file path.
QLabel *pathLabel = new QLabel(tr("Target &path:"));
m_pathEdit = new QLineEdit();
pathLabel->setBuddy(m_pathEdit);
m_browseBtn = new QPushButton(tr("&Browse"));
connect(m_browseBtn, &QPushButton::clicked,
this, &VExporter::handleBrowseBtnClicked);
// Page layout.
QLabel *layoutLabel = new QLabel(tr("Page layout:"));
m_layoutLabel = new QLabel();
m_layoutBtn = new QPushButton(tr("&Settings"));
#ifndef QT_NO_PRINTER
connect(m_layoutBtn, &QPushButton::clicked,
this, &VExporter::handleLayoutBtnClicked);
#else
m_layoutBtn->hide();
#endif
// Progress.
m_proLabel = new QLabel(this);
m_proBar = new QProgressBar(this);
// Ok is the default button.
m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(m_btnBox, &QDialogButtonBox::accepted, this, &VExporter::startExport);
connect(m_btnBox, &QDialogButtonBox::rejected, this, &VExporter::cancelExport);
QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok);
m_pathEdit->setMinimumWidth(okBtn->sizeHint().width() * 3);
QGridLayout *mainLayout = new QGridLayout();
mainLayout->addWidget(m_webViewer, 0, 0, 1, 3);
mainLayout->addWidget(m_infoLabel, 1, 0, 1, 3);
mainLayout->addWidget(pathLabel, 2, 0);
mainLayout->addWidget(m_pathEdit, 2, 1);
mainLayout->addWidget(m_browseBtn, 2, 2);
mainLayout->addWidget(layoutLabel, 3, 0);
mainLayout->addWidget(m_layoutLabel, 3, 1);
mainLayout->addWidget(m_layoutBtn, 3, 2);
mainLayout->addWidget(m_proLabel, 4, 1, 1, 2);
mainLayout->addWidget(m_proBar, 5, 1, 1, 2);
mainLayout->addWidget(m_btnBox, 6, 1, 1, 2);
// Only use VWebView to do the conversion.
m_webViewer->hide();
m_proLabel->hide();
m_proBar->hide();
setLayout(mainLayout);
mainLayout->setSizeConstraint(QLayout::SetFixedSize);
setWindowTitle(tr("Export Note"));
updatePageLayoutLabel();
}
static QString exportTypeStr(ExportType p_type)
{
if (p_type == ExportType::PDF) {
return "PDF";
} else {
return "HTML";
}
}
void VExporter::handleBrowseBtnClicked()
{
QFileInfo fi(getFilePath());
QString fileType = m_type == ExportType::PDF ?
tr("Portable Document Format (*.pdf)") :
tr("WebPage, Complete (*.html)");
QString path = QFileDialog::getSaveFileName(this, tr("Export As"),
fi.absolutePath(),
fileType);
if (path.isEmpty()) {
return;
}
setFilePath(path);
s_defaultPathDir = VUtils::basePathFromPath(path);
}
void VExporter::handleLayoutBtnClicked()
{
#ifndef QT_NO_PRINTER
QPrinter printer;
printer.setPageLayout(m_pageLayout);
QPageSetupDialog dlg(&printer, this);
if (dlg.exec() != QDialog::Accepted) {
return;
}
m_pageLayout.setPageSize(printer.pageLayout().pageSize());
m_pageLayout.setOrientation(printer.pageLayout().orientation());
updatePageLayoutLabel();
#endif
}
void VExporter::updatePageLayoutLabel()
{
m_layoutLabel->setText(QString("%1, %2").arg(m_pageLayout.pageSize().name())
.arg(m_pageLayout.orientation() == QPageLayout::Portrait ?
tr("Portrait") : tr("Landscape")));
}
QString VExporter::getFilePath() const
{
return QDir::cleanPath(m_pathEdit->text());
}
void VExporter::setFilePath(const QString &p_path)
{
m_pathEdit->setText(QDir::toNativeSeparators(p_path));
}
void VExporter::exportNote(VFile *p_file, ExportType p_type)
{
m_file = p_file;
m_type = p_type;
m_source = ExportSource::Note;
if (!m_file || m_file->getDocType() != DocType::Markdown) {
// Do not support non-Markdown note now.
m_btnBox->button(QDialogButtonBox::Ok)->setEnabled(false);
return;
}
m_infoLabel->setText(tr("Export note <span style=\"%1\">%2</span> as %3.")
.arg(vconfig.c_dataTextStyle)
.arg(m_file->getName())
.arg(exportTypeStr(p_type)));
setWindowTitle(tr("Export As %1").arg(exportTypeStr(p_type)));
setFilePath(QDir(s_defaultPathDir).filePath(QFileInfo(p_file->retrivePath()).baseName() +
"." + exportTypeStr(p_type).toLower()));
}
void VExporter::setupMarkdownViewer()
{
m_webViewer = new VWebView(NULL, this);
VPreviewPage *page = new VPreviewPage(this);
m_webViewer->setPage(page);
QWebChannel *channel = new QWebChannel(this);
channel->registerObject(QStringLiteral("content"), &m_document);
page->setWebChannel(channel);
connect(&m_document, &VDocument::loadFinished,
this, &VExporter::readyToExport);
QString jsFile, extraFile;
switch (m_mdType) {
case MarkdownConverterType::Marked:
jsFile = "qrc" + VNote::c_markedJsFile;
extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
break;
case MarkdownConverterType::Hoedown:
jsFile = "qrc" + VNote::c_hoedownJsFile;
// Use Marked to highlight code blocks.
extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
break;
case MarkdownConverterType::MarkdownIt:
jsFile = "qrc" + VNote::c_markdownitJsFile;
extraFile = "<script src=\"qrc" + VNote::c_markdownitExtraFile + "\"></script>\n" +
"<script src=\"qrc" + VNote::c_markdownitAnchorExtraFile + "\"></script>\n" +
"<script src=\"qrc" + VNote::c_markdownitTaskListExtraFile + "\"></script>\n";
break;
case MarkdownConverterType::Showdown:
jsFile = "qrc" + VNote::c_showdownJsFile;
extraFile = "<script src=\"qrc" + VNote::c_showdownExtraFile + "\"></script>\n" +
"<script src=\"qrc" + VNote::c_showdownAnchorExtraFile + "\"></script>\n";
break;
default:
Q_ASSERT(false);
}
if (vconfig.getEnableMermaid()) {
extraFile += "<link rel=\"stylesheet\" type=\"text/css\" href=\"qrc" + VNote::c_mermaidCssFile +
"\"/>\n" + "<script src=\"qrc" + VNote::c_mermaidApiJsFile + "\"></script>\n" +
"<script>var VEnableMermaid = true;</script>\n";
}
if (vconfig.getEnableMathjax()) {
extraFile += "<script type=\"text/x-mathjax-config\">"
"MathJax.Hub.Config({\n"
" tex2jax: {inlineMath: [['$','$'], ['\\\\(','\\\\)']]},\n"
" showProcessingMessages: false,\n"
" messageStyle: \"none\"});\n"
"</script>\n"
"<script type=\"text/javascript\" async src=\"" + VNote::c_mathjaxJsFile + "\"></script>\n" +
"<script>var VEnableMathjax = true;</script>\n";
}
if (vconfig.getEnableImageCaption()) {
extraFile += "<script>var VEnableImageCaption = true;</script>\n";
}
m_htmlTemplate = VNote::s_markdownTemplatePDF;
m_htmlTemplate.replace(c_htmlJSHolder, jsFile);
if (!extraFile.isEmpty()) {
m_htmlTemplate.replace(c_htmlExtraHolder, extraFile);
}
}
void VExporter::updateWebViewer(VFile *p_file)
{
m_document.setFile(p_file);
// Need to generate HTML using Hoedown.
if (m_mdType == MarkdownConverterType::Hoedown) {
VMarkdownConverter mdConverter;
QString toc;
QString html = mdConverter.generateHtml(p_file->getContent(),
vconfig.getMarkdownExtensions(),
toc);
m_document.setHtml(html);
}
m_webViewer->setHtml(m_htmlTemplate, p_file->getBaseUrl());
}
void VExporter::readyToExport()
{
Q_ASSERT(!m_webReady);
m_webReady = true;
}
void VExporter::startExport()
{
enableUserInput(false);
V_ASSERT(m_state == ExportState::Idle);
m_state = ExportState::Busy;
if (m_source == ExportSource::Note) {
V_ASSERT(m_file);
bool isOpened = m_file->isOpened();
if (!isOpened && !m_file->open()) {
goto exit;
}
m_webReady = false;
updateWebViewer(m_file);
// Update progress info.
m_proLabel->setText(tr("Exporting %1").arg(m_file->getName()));
m_proBar->setMinimum(0);
m_proBar->setMaximum(100);
m_proBar->reset();
m_proLabel->show();
m_proBar->show();
while (!m_webReady) {
VUtils::sleepWait(100);
if (m_proBar->value() < 70) {
m_proBar->setValue(m_proBar->value() + 1);
}
if (m_state == ExportState::Cancelled) {
goto exit;
}
}
// Wait to ensure Web side is really ready.
VUtils::sleepWait(200);
if (m_state == ExportState::Cancelled) {
goto exit;
}
m_proBar->setValue(80);
exportToPDF(m_webViewer, getFilePath(), m_pageLayout);
m_proBar->setValue(100);
m_webReady = false;
if (!isOpened) {
m_file->close();
}
}
exit:
m_proLabel->setText("");
m_proBar->reset();
m_proLabel->hide();
m_proBar->hide();
enableUserInput(true);
if (m_state == ExportState::Cancelled) {
reject();
} else {
accept();
}
m_state = ExportState::Idle;
}
void VExporter::cancelExport()
{
if (m_state == ExportState::Idle) {
reject();
} else {
m_state = ExportState::Cancelled;
}
}
void 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());
QFile file(p_filePath);
if (!file.open(QFile::WriteOnly)) {
pdfPrinted = -1;
return;
}
file.write(p_result.data(), p_result.size());
file.close();
pdfPrinted = 1;
}, p_layout);
while (pdfPrinted == 0) {
VUtils::sleepWait(100);
if (m_state == ExportState::Cancelled) {
break;
}
}
qDebug() << "export to PDF" << p_filePath << "state" << pdfPrinted;
}
void VExporter::enableUserInput(bool p_enabled)
{
m_btnBox->button(QDialogButtonBox::Ok)->setEnabled(p_enabled);
m_pathEdit->setEnabled(p_enabled);
m_browseBtn->setEnabled(p_enabled);
m_layoutBtn->setEnabled(p_enabled);
}

101
src/vexporter.h Normal file
View File

@ -0,0 +1,101 @@
#ifndef VEXPORTER_H
#define VEXPORTER_H
#include <QDialog>
#include <QPageLayout>
#include <QString>
#include "vconfigmanager.h"
#include "vdocument.h"
class VWebView;
class VFile;
class QLineEdit;
class QLabel;
class QDialogButtonBox;
class QPushButton;
class QProgressBar;
enum class ExportType
{
PDF = 0,
HTML
};
enum class ExportSource
{
Note = 0,
Directory,
Notebook,
Invalid
};
enum class ExportState
{
Idle = 0,
Cancelled,
Busy
};
class VExporter : public QDialog
{
Q_OBJECT
public:
explicit VExporter(MarkdownConverterType p_mdType = MarkdownIt, QWidget *p_parent = 0);
void exportNote(VFile *p_file, ExportType p_type);
private slots:
void handleBrowseBtnClicked();
void handleLayoutBtnClicked();
void startExport();
void cancelExport();
private:
void setupUI();
// Init m_webViewer, m_document, and m_htmlTemplate.
void setupMarkdownViewer();
void updatePageLayoutLabel();
void setFilePath(const QString &p_path);
QString getFilePath() const;
void updateWebViewer(VFile *p_file);
void readyToExport();
void enableUserInput(bool p_enabled);
void exportToPDF(VWebView *p_webViewer, const QString &p_filePath, const QPageLayout &p_layout);
VWebView *m_webViewer;
VDocument m_document;
MarkdownConverterType m_mdType;
QString m_htmlTemplate;
VFile *m_file;
ExportType m_type;
ExportSource m_source;
bool m_webReady;
ExportState m_state;
QLabel *m_infoLabel;
QLineEdit *m_pathEdit;
QPushButton *m_browseBtn;
QLabel *m_layoutLabel;
QPushButton *m_layoutBtn;
QDialogButtonBox *m_btnBox;
// Progress label and bar.
QLabel *m_proLabel;
QProgressBar *m_proBar;
QPageLayout m_pageLayout;
// The default directory.
static QString s_defaultPathDir;
};
#endif // VEXPORTER_H

View File

@ -3,6 +3,7 @@
#include <QDir> #include <QDir>
#include <QDebug> #include <QDebug>
#include <QTextEdit> #include <QTextEdit>
#include <QFileInfo>
#include "utils/vutils.h" #include "utils/vutils.h"
VFile::VFile(const QString &p_name, QObject *p_parent, VFile::VFile(const QString &p_name, QObject *p_parent,
@ -214,3 +215,25 @@ bool VFile::isInternalImageFolder(const QString &p_path) const
{ {
return VUtils::basePathFromPath(p_path) == getDirectory()->retrivePath(); return VUtils::basePathFromPath(p_path) == getDirectory()->retrivePath();
} }
QUrl VFile::getBaseUrl() const
{
// Need to judge the path: Url, local file, resource file.
QUrl baseUrl;
QString basePath = retriveBasePath();
QFileInfo pathInfo(basePath);
if (pathInfo.exists()) {
if (pathInfo.isNativePath()) {
// Local file.
baseUrl = QUrl::fromLocalFile(basePath + QDir::separator());
} else {
// Resource file.
baseUrl = QUrl("qrc" + basePath + QDir::separator());
}
} else {
// Url.
baseUrl = QUrl(basePath + QDir::separator());
}
return baseUrl;
}

View File

@ -3,6 +3,7 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QUrl>
#include "vdirectory.h" #include "vdirectory.h"
#include "vconstants.h" #include "vconstants.h"
@ -40,6 +41,9 @@ public:
bool isOpened() const; bool isOpened() const;
FileType getType() const; FileType getType() const;
// Return the base URL for this file when loaded in VWebView.
QUrl getBaseUrl() const;
// Whether the directory @p_path is an internal image folder of this file. // Whether the directory @p_path is an internal image folder of this file.
// It is true only when the folder is in the same directory as the parent // It is true only when the folder is in the same directory as the parent
// directory of this file. // directory of this file.

View File

@ -18,6 +18,7 @@
#include "vcaptain.h" #include "vcaptain.h"
#include "vedittab.h" #include "vedittab.h"
#include "vwebview.h" #include "vwebview.h"
#include "vexporter.h"
extern VConfigManager vconfig; extern VConfigManager vconfig;
@ -453,6 +454,18 @@ void VMainWindow::initFileMenu()
fileMenu->addSeparator(); fileMenu->addSeparator();
// Export as PDF.
m_exportAsPDFAct = new QAction(QIcon(":/resources/icons/export_pdf.svg"),
tr("Export As &PDF"), this);
m_exportAsPDFAct->setToolTip(tr("Export current note as PDF file"));
connect(m_exportAsPDFAct, &QAction::triggered,
this, &VMainWindow::exportAsPDF);
m_exportAsPDFAct->setEnabled(false);
fileMenu->addAction(m_exportAsPDFAct);
fileMenu->addSeparator();
// Print. // Print.
m_printAct = new QAction(QIcon(":/resources/icons/print.svg"), m_printAct = new QAction(QIcon(":/resources/icons/print.svg"),
tr("&Print"), this); tr("&Print"), this);
@ -1016,6 +1029,7 @@ void VMainWindow::updateActionStateFromTabStatusChange(const VFile *p_file,
bool p_editMode) bool p_editMode)
{ {
m_printAct->setEnabled(p_file && p_file->getDocType() == DocType::Markdown); m_printAct->setEnabled(p_file && p_file->getDocType() == DocType::Markdown);
m_exportAsPDFAct->setEnabled(p_file && p_file->getDocType() == DocType::Markdown);
editNoteAct->setVisible(p_file && p_file->isModifiable() && !p_editMode); editNoteAct->setVisible(p_file && p_file->isModifiable() && !p_editMode);
discardExitAct->setVisible(p_file && p_editMode); discardExitAct->setVisible(p_file && p_editMode);
@ -1393,3 +1407,13 @@ void VMainWindow::printNote()
} }
} }
void VMainWindow::exportAsPDF()
{
V_ASSERT(m_curTab);
V_ASSERT(m_curFile);
VExporter exporter(m_curTab->getMarkdownConverterType(), this);
exporter.exportNote(m_curFile, ExportType::PDF);
exporter.exec();
}

View File

@ -81,6 +81,7 @@ private slots:
void enableImageConstraint(bool p_checked); void enableImageConstraint(bool p_checked);
void enableImageCaption(bool p_checked); void enableImageCaption(bool p_checked);
void printNote(); void printNote();
void exportAsPDF();
protected: protected:
void closeEvent(QCloseEvent *event) Q_DECL_OVERRIDE; void closeEvent(QCloseEvent *event) Q_DECL_OVERRIDE;
@ -155,6 +156,7 @@ private:
QAction *expandViewAct; QAction *expandViewAct;
QAction *m_importNoteAct; QAction *m_importNoteAct;
QAction *m_printAct; QAction *m_printAct;
QAction *m_exportAsPDFAct;
QAction *m_insertImageAct; QAction *m_insertImageAct;
QAction *m_findReplaceAct; QAction *m_findReplaceAct;

View File

@ -1,4 +1,5 @@
#include "vmarkdownconverter.h" #include "vmarkdownconverter.h"
#include <QRegularExpression>
VMarkdownConverter::VMarkdownConverter() VMarkdownConverter::VMarkdownConverter()
{ {
@ -36,11 +37,35 @@ QString VMarkdownConverter::generateHtml(const QString &markdown, hoedown_extens
return html; return html;
} }
QString VMarkdownConverter::generateHtml(const QString &markdown, hoedown_extensions options, QString &toc)
{
if (markdown.isEmpty()) {
return QString();
}
QString html = generateHtml(markdown, options);
QRegularExpression tocExp("<p>\\[TOC\\]<\\/p>", QRegularExpression::CaseInsensitiveOption);
toc = generateToc(markdown, options);
html.replace(tocExp, toc);
return html;
}
static void processToc(QString &p_toc)
{
// Hoedown will add '\n'.
p_toc.replace("\n", "");
// Hoedown will translate `_` in title to `<em>`.
p_toc.replace("<em>", "_");
p_toc.replace("</em>", "_");
}
QString VMarkdownConverter::generateToc(const QString &markdown, hoedown_extensions options) QString VMarkdownConverter::generateToc(const QString &markdown, hoedown_extensions options)
{ {
if (markdown.isEmpty()) { if (markdown.isEmpty()) {
return QString(); return QString();
} }
hoedown_document *document = hoedown_document_new(tocRenderer, options, nestingLevel); hoedown_document *document = hoedown_document_new(tocRenderer, options, nestingLevel);
QByteArray data = markdown.toUtf8(); QByteArray data = markdown.toUtf8();
hoedown_buffer *outBuf = hoedown_buffer_new(16); hoedown_buffer *outBuf = hoedown_buffer_new(16);
@ -48,5 +73,8 @@ QString VMarkdownConverter::generateToc(const QString &markdown, hoedown_extensi
hoedown_document_free(document); hoedown_document_free(document);
QString toc = QString::fromUtf8(hoedown_buffer_cstr(outBuf)); QString toc = QString::fromUtf8(hoedown_buffer_cstr(outBuf));
hoedown_buffer_free(outBuf); hoedown_buffer_free(outBuf);
processToc(toc);
return toc; return toc;
} }

View File

@ -14,10 +14,13 @@ public:
VMarkdownConverter(); VMarkdownConverter();
~VMarkdownConverter(); ~VMarkdownConverter();
QString generateHtml(const QString &markdown, hoedown_extensions options); QString generateHtml(const QString &markdown, hoedown_extensions options, QString &toc);
QString generateToc(const QString &markdown, hoedown_extensions options); QString generateToc(const QString &markdown, hoedown_extensions options);
private: private:
QString generateHtml(const QString &markdown, hoedown_extensions options);
// VMarkdownDocument *generateDocument(const QString &markdown); // VMarkdownDocument *generateDocument(const QString &markdown);
hoedown_html_flags hoedownHtmlFlags; hoedown_html_flags hoedownHtmlFlags;
int nestingLevel; int nestingLevel;

View File

@ -16,6 +16,7 @@
extern VConfigManager vconfig; extern VConfigManager vconfig;
QString VNote::s_markdownTemplate; QString VNote::s_markdownTemplate;
QString VNote::s_markdownTemplatePDF;
const QString VNote::c_hoedownJsFile = ":/resources/hoedown.js"; const QString VNote::c_hoedownJsFile = ":/resources/hoedown.js";
const QString VNote::c_markedJsFile = ":/resources/marked.js"; const QString VNote::c_markedJsFile = ":/resources/marked.js";
@ -170,14 +171,22 @@ void VNote::updateTemplate()
cssStyle += "img { max-width: 100% !important; height: auto !important; }\n"; cssStyle += "img { max-width: 100% !important; height: auto !important; }\n";
} }
QString styleHolder("<!-- BACKGROUND_PLACE_HOLDER -->"); const QString styleHolder("<!-- BACKGROUND_PLACE_HOLDER -->");
QString cssHolder("CSS_PLACE_HOLDER"); const QString cssHolder("CSS_PLACE_HOLDER");
s_markdownTemplate = VUtils::readFileFromDisk(c_markdownTemplatePath); s_markdownTemplate = VUtils::readFileFromDisk(c_markdownTemplatePath);
s_markdownTemplate.replace(cssHolder, vconfig.getTemplateCssUrl()); s_markdownTemplate.replace(cssHolder, vconfig.getTemplateCssUrl());
s_markdownTemplatePDF = s_markdownTemplate;
if (!cssStyle.isEmpty()) { if (!cssStyle.isEmpty()) {
s_markdownTemplate.replace(styleHolder, cssStyle); s_markdownTemplate.replace(styleHolder, cssStyle);
} }
// Shoudl not display scrollbar in PDF.
cssStyle += "pre code { white-space: pre-wrap !important; "
"word-break: break-all !important; }\n";
s_markdownTemplatePDF.replace(styleHolder, cssStyle);
} }
const QVector<VNotebook *> &VNote::getNotebooks() const const QVector<VNotebook *> &VNote::getNotebooks() const

View File

@ -28,6 +28,7 @@ public:
void initTemplate(); void initTemplate();
static QString s_markdownTemplate; static QString s_markdownTemplate;
static QString s_markdownTemplatePDF;
// Hoedown // Hoedown
static const QString c_hoedownJsFile; static const QString c_hoedownJsFile;

View File

@ -20,7 +20,7 @@ void VWebView::contextMenuEvent(QContextMenuEvent *p_event)
const QList<QAction *> actions = menu->actions(); const QList<QAction *> actions = menu->actions();
if (!hasSelection() && m_file->isModifiable()) { if (!hasSelection() && m_file && m_file->isModifiable()) {
QAction *editAct= new QAction(QIcon(":/resources/icons/edit_note.svg"), QAction *editAct= new QAction(QIcon(":/resources/icons/edit_note.svg"),
tr("&Edit"), this); tr("&Edit"), this);
editAct->setToolTip(tr("Edit current note")); editAct->setToolTip(tr("Edit current note"));

View File

@ -9,6 +9,7 @@ class VWebView : public QWebEngineView
{ {
Q_OBJECT Q_OBJECT
public: public:
// @p_file could be NULL.
explicit VWebView(VFile *p_file, QWidget *p_parent = Q_NULLPTR); explicit VWebView(VFile *p_file, QWidget *p_parent = Q_NULLPTR);
signals: signals: