export: support HTML format

This commit is contained in:
Le Tan 2018-02-24 23:08:27 +08:00
parent 60635fe5e7
commit 1fe7567d79
18 changed files with 323 additions and 82 deletions

View File

@ -289,7 +289,8 @@ void VExportDialog::startExport()
m_consoleEdit->clear(); m_consoleEdit->clear();
appendLogLine(tr("Export to %1.").arg(outputFolder)); appendLogLine(tr("Export to %1.").arg(outputFolder));
if (opt.m_format == ExportFormat::PDF) { if (opt.m_format == ExportFormat::PDF
|| opt.m_format == ExportFormat::HTML) {
m_exporter->prepareExport(opt); m_exporter->prepareExport(opt);
} }
@ -406,6 +407,10 @@ int VExportDialog::doExport(VFile *p_file,
ret = doExportPDF(p_file, p_opt, p_outputFolder, p_errMsg); ret = doExportPDF(p_file, p_opt, p_outputFolder, p_errMsg);
break; break;
case (int)ExportFormat::HTML:
ret = doExportHTML(p_file, p_opt, p_outputFolder, p_errMsg);
break;
default: default:
break; break;
} }
@ -628,6 +633,40 @@ int VExportDialog::doExportPDF(VFile *p_file,
} }
} }
int VExportDialog::doExportHTML(VFile *p_file,
const ExportOption &p_opt,
const QString &p_outputFolder,
QString *p_errMsg)
{
Q_UNUSED(p_opt);
QString srcFilePath(p_file->fetchPath());
if (p_file->getDocType() != DocType::Markdown) {
LOGERR(tr("Skip exporting non-Markdown file %1 as HTML.").arg(srcFilePath));
return 0;
}
if (!VUtils::makePath(p_outputFolder)) {
LOGERR(tr("Fail to create directory %1.").arg(p_outputFolder));
return 0;
}
// Get output file.
QString suffix = ".html";
QString name = VUtils::getFileNameWithSequence(p_outputFolder,
QFileInfo(p_file->getName()).completeBaseName() + suffix);
QString outputPath = QDir(p_outputFolder).filePath(name);
if (m_exporter->exportHTML(p_file, p_opt, outputPath, p_errMsg)) {
appendLogLine(tr("Note %1 exported to %2.").arg(srcFilePath).arg(outputPath));
return 1;
} else {
appendLogLine(tr("Fail to export note %1.").arg(srcFilePath));
return 0;
}
}
bool VExportDialog::checkUserAction() bool VExportDialog::checkUserAction()
{ {
if (m_askedToStop) { if (m_askedToStop) {

View File

@ -133,6 +133,11 @@ private:
const QString &p_outputFolder, const QString &p_outputFolder,
QString *p_errMsg = NULL); QString *p_errMsg = NULL);
int doExportHTML(VFile *p_file,
const ExportOption &p_opt,
const QString &p_outputFolder,
QString *p_errMsg = NULL);
// Return false if we could not continue. // Return false if we could not continue.
bool checkUserAction(); bool checkUserAction();

View File

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

View File

@ -127,7 +127,7 @@ var updateText = function(text) {
var highlightText = function(text, id, timeStamp) { var highlightText = function(text, id, timeStamp) {
var html = mdit.render(text); var html = mdit.render(text);
content.highlightTextCB(html, id, timeStamp); content.highlightTextCB(html, id, timeStamp);
} };
var textToHtml = function(text) { var textToHtml = function(text) {
var html = mdit.render(text); var html = mdit.render(text);
@ -139,4 +139,4 @@ var textToHtml = function(text) {
container.innerHTML = ""; container.innerHTML = "";
content.textToHtmlCB(text, html); content.textToHtmlCB(text, html);
} };

View File

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

View File

@ -40,6 +40,26 @@ if (typeof VEnableImageCaption == 'undefined') {
VEnableImageCaption = false; VEnableImageCaption = false;
} }
var headContent = function() {
var styles = "<style type=\"text/css\">\n";
for (var i = 0; i < document.styleSheets.length; ++i) {
var styleSheet = document.styleSheets[i];
if (styleSheet.cssRules) {
for (var j = 0; j < styleSheet.cssRules.length; ++j) {
styles = styles + styleSheet.cssRules[j].cssText + "\n";
}
}
}
var styles = styles + "</style>";
return styles;
};
var htmlContent = function() {
content.htmlContentCB(headContent(), placeholder.innerHTML);
};
new QWebChannel(qt.webChannelTransport, new QWebChannel(qt.webChannelTransport,
function(channel) { function(channel) {
content = channel.objects.content; content = channel.objects.content;
@ -62,6 +82,10 @@ new QWebChannel(qt.webChannelTransport,
content.requestTextToHtml.connect(textToHtml); content.requestTextToHtml.connect(textToHtml);
content.noticeReadyToTextToHtml(); content.noticeReadyToTextToHtml();
} }
if (typeof htmlContent == "function") {
content.requestHtmlContent.connect(htmlContent);
}
}); });
var VHighlightedAnchorClass = 'highlighted-anchor'; var VHighlightedAnchorClass = 'highlighted-anchor';
@ -447,7 +471,7 @@ var renderMermaidOne = function(code) {
mermaidIdx++; mermaidIdx++;
try { try {
// Do not increment mermaidIdx here. // Do not increment mermaidIdx here.
var graph = mermaidAPI.render('mermaid-diagram-' + mermaidIdx, code.innerText, function(){}); var graph = mermaidAPI.render('mermaid-diagram-' + mermaidIdx, code.textContent, function(){});
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
return false; return false;
@ -493,7 +517,7 @@ var renderFlowchartOne = function(code) {
// Flowchart code block. // Flowchart code block.
flowchartIdx++; flowchartIdx++;
try { try {
var graph = flowchart.parse(code.innerText); var graph = flowchart.parse(code.textContent);
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
return false; return false;
@ -525,7 +549,7 @@ var renderFlowchartOne = function(code) {
var isImageBlock = function(img) { var isImageBlock = function(img) {
var pn = img.parentNode; var pn = img.parentNode;
return (pn.children.length == 1) && (pn.innerText == ''); return (pn.children.length == 1) && (pn.textContent == '');
}; };
var isImageWithBr = function(img) { var isImageWithBr = function(img) {
@ -617,7 +641,7 @@ var insertImageCaption = function() {
// Add caption. // Add caption.
var captionDiv = document.createElement('div'); var captionDiv = document.createElement('div');
captionDiv.classList.add(VImageCaptionClass); captionDiv.classList.add(VImageCaptionClass);
captionDiv.innerText = img.alt; captionDiv.textContent = img.alt;
img.insertAdjacentElement('afterend', captionDiv); img.insertAdjacentElement('afterend', captionDiv);
} }
} }

View File

@ -203,7 +203,7 @@ enable_wildcard_in_simple_search=true
[web] [web]
; Location and configuration for Mathjax ; Location and configuration for Mathjax
mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_HTMLorMML
; Styles to be removed when copied ; Styles to be removed when copied
; style1,style2,style3 ; style1,style2,style3

View File

@ -690,6 +690,11 @@ QString VUtils::generateHtmlTemplate(const QString &p_template,
return htmlTemplate; return htmlTemplate;
} }
QString VUtils::generateExportHtmlTemplate(const QString &p_renderBg)
{
return VNote::generateExportHtmlTemplate(g_config->getRenderBackgroundColor(p_renderBg));
}
QString VUtils::getFileNameWithSequence(const QString &p_directory, QString VUtils::getFileNameWithSequence(const QString &p_directory,
const QString &p_baseFileName, const QString &p_baseFileName,
bool p_completeBaseName) bool p_completeBaseName)

View File

@ -174,6 +174,9 @@ public:
const QString &p_renderCodeBlockStyle, const QString &p_renderCodeBlockStyle,
bool p_isPDF); bool p_isPDF);
// @p_renderBg is the background name.
static QString generateExportHtmlTemplate(const QString &p_renderBg);
static QString generateSimpleHtmlTemplate(const QString &p_body); static QString generateSimpleHtmlTemplate(const QString &p_body);
// Get an available file name in @p_directory with base @p_baseFileName. // Get an available file name in @p_directory with base @p_baseFileName.

View File

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

View File

@ -82,6 +82,11 @@ void VDocument::textToHtmlAsync(const QString &p_text)
emit requestTextToHtml(p_text); emit requestTextToHtml(p_text);
} }
void VDocument::getHtmlContentAsync()
{
emit requestHtmlContent();
}
void VDocument::textToHtmlCB(const QString &p_text, const QString &p_html) void VDocument::textToHtmlCB(const QString &p_text, const QString &p_html)
{ {
emit textToHtmlFinished(p_text, p_html); emit textToHtmlFinished(p_text, p_html);
@ -108,3 +113,8 @@ void VDocument::finishLogics()
qDebug() << "Web side finished logics"; qDebug() << "Web side finished logics";
emit logicsFinished(); emit logicsFinished();
} }
void VDocument::htmlContentCB(const QString &p_head, const QString &p_body)
{
emit htmlContentFinished(p_head, p_body);
}

View File

@ -38,6 +38,9 @@ public:
bool isReadyToTextToHtml() const; bool isReadyToTextToHtml() const;
// Request to get the HTML content.
void getHtmlContentAsync();
public slots: public slots:
// Will be called in the HTML side // Will be called in the HTML side
@ -67,6 +70,8 @@ public slots:
// But the page may not finish loading, such as images. // But the page may not finish loading, such as images.
void finishLogics(); void finishLogics();
void htmlContentCB(const QString &p_head, const QString &p_body);
signals: signals:
void textChanged(const QString &text); void textChanged(const QString &text);
@ -95,6 +100,11 @@ signals:
void textToHtmlFinished(const QString &p_text, const QString &p_html); void textToHtmlFinished(const QString &p_text, const QString &p_html);
void requestHtmlContent();
void htmlContentFinished(const QString &p_headContent,
const QString &p_bodyContent);
private: private:
QString m_toc; QString m_toc;
QString m_header; QString m_header;

View File

@ -29,6 +29,9 @@ void VExporter::prepareExport(const ExportOption &p_opt)
p_opt.m_renderStyle, p_opt.m_renderStyle,
p_opt.m_renderCodeBlockStyle, p_opt.m_renderCodeBlockStyle,
p_opt.m_format == ExportFormat::PDF); p_opt.m_format == ExportFormat::PDF);
m_exportHtmlTemplate = VUtils::generateExportHtmlTemplate(p_opt.m_renderBg);
m_pageLayout = *(p_opt.m_layout); m_pageLayout = *(p_opt.m_layout);
} }
@ -36,6 +39,117 @@ bool VExporter::exportPDF(VFile *p_file,
const ExportOption &p_opt, const ExportOption &p_opt,
const QString &p_outputFile, const QString &p_outputFile,
QString *p_errMsg) 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);
}
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);
m_webDocument = new VDocument(p_file, m_webViewer);
connect(m_webDocument, &VDocument::logicsFinished,
this, &VExporter::handleLogicsFinished);
QWebChannel *channel = new QWebChannel(m_webViewer);
channel->registerObject(QStringLiteral("content"), m_webDocument);
page->setWebChannel(channel);
// 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);
m_webDocument->setHtml(html);
}
m_webViewer->setHtml(m_htmlTemplate, p_file->getBaseUrl());
}
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;
}
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());
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;
}
}
return pdfPrinted == 1;
}
bool VExporter::exportViaWebView(VFile *p_file,
const ExportOption &p_opt,
const QString &p_outputFile,
QString *p_errMsg)
{ {
Q_UNUSED(p_errMsg); Q_UNUSED(p_errMsg);
@ -74,9 +188,24 @@ bool VExporter::exportPDF(VFile *p_file,
} }
{ {
bool exportRet = exportToPDF(m_webViewer,
bool exportRet = false;
switch (p_opt.m_format) {
case ExportFormat::PDF:
exportRet = exportToPDF(m_webViewer,
p_outputFile, p_outputFile,
m_pageLayout); m_pageLayout);
break;
case ExportFormat::HTML:
exportRet = exportToHTML(m_webViewer,
m_webDocument,
p_outputFile);
break;
default:
break;
}
clearNoteState(); clearNoteState();
@ -89,6 +218,7 @@ bool VExporter::exportPDF(VFile *p_file,
} else { } else {
m_state = ExportState::Failed; m_state = ExportState::Failed;
} }
} }
exit: exit:
@ -103,91 +233,42 @@ exit:
return ret; return ret;
} }
void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt) bool VExporter::exportToHTML(VWebView *p_webViewer,
VDocument *p_webDocument,
const QString &p_filePath)
{ {
Q_ASSERT(!m_webViewer); Q_UNUSED(p_webViewer);
int htmlExported = 0;
m_webViewer = new VWebView(p_file, static_cast<QWidget *>(parent())); connect(p_webDocument, &VDocument::htmlContentFinished,
m_webViewer->hide(); this, [&, this](const QString &p_headContent, const QString &p_bodyContent) {
if (p_bodyContent.isEmpty() || this->m_state == ExportState::Cancelled) {
VPreviewPage *page = new VPreviewPage(m_webViewer); htmlExported = -1;
m_webViewer->setPage(page);
connect(page, &VPreviewPage::loadFinished,
this, &VExporter::handleLoadFinished);
VDocument *document = new VDocument(p_file, m_webViewer);
connect(document, &VDocument::logicsFinished,
this, &VExporter::handleLogicsFinished);
QWebChannel *channel = new QWebChannel(m_webViewer);
channel->registerObject(QStringLiteral("content"), document);
page->setWebChannel(channel);
// 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);
document->setHtml(html);
}
m_webViewer->setHtml(m_htmlTemplate, p_file->getBaseUrl());
}
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()
{
if (m_webViewer) {
delete m_webViewer;
m_webViewer = NULL;
}
}
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; return;
} }
V_ASSERT(!p_filePath.isEmpty()); Q_ASSERT(!p_filePath.isEmpty());
QFile file(p_filePath); QFile file(p_filePath);
if (!file.open(QFile::WriteOnly)) { if (!file.open(QFile::WriteOnly)) {
pdfPrinted = -1; htmlExported = -1;
return; return;
} }
file.write(p_result.data(), p_result.size()); QString html(m_exportHtmlTemplate);
html.replace(HtmlHolder::c_headHolder, p_headContent);
html.replace(HtmlHolder::c_bodyHolder, p_bodyContent);
file.write(html.toUtf8());
file.close(); file.close();
pdfPrinted = 1; htmlExported = 1;
}, p_layout); });
while (pdfPrinted == 0) { p_webDocument->getHtmlContentAsync();
while (htmlExported == 0) {
VUtils::sleepWait(100); VUtils::sleepWait(100);
if (m_state == ExportState::Cancelled) { if (m_state == ExportState::Cancelled) {
@ -195,6 +276,5 @@ bool VExporter::exportToPDF(VWebView *p_webViewer,
} }
} }
return pdfPrinted == 1; return htmlExported == 1;
} }

View File

@ -8,6 +8,7 @@
class QWidget; class QWidget;
class VWebView; class VWebView;
class VDocument;
class VExporter : public QObject class VExporter : public QObject
{ {
@ -21,6 +22,11 @@ public:
const QString &p_outputFile, const QString &p_outputFile,
QString *p_errMsg = NULL); QString *p_errMsg = NULL);
bool exportHTML(VFile *p_file,
const ExportOption &p_opt,
const QString &p_outputFile,
QString *p_errMsg = NULL);
private slots: private slots:
void handleLogicsFinished(); void handleLogicsFinished();
@ -57,17 +63,31 @@ private:
bool isNoteStateFailed() 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, bool exportToPDF(VWebView *p_webViewer,
const QString &p_filePath, const QString &p_filePath,
const QPageLayout &p_layout); const QPageLayout &p_layout);
bool exportToHTML(VWebView *p_webViewer,
VDocument *p_webDocument,
const QString &p_filePath);
QPageLayout m_pageLayout; QPageLayout m_pageLayout;
// Will be allocated and free for each conversion. // Will be allocated and free for each conversion.
VWebView *m_webViewer; VWebView *m_webViewer;
VDocument *m_webDocument;
QString m_htmlTemplate; QString m_htmlTemplate;
// Template to hold the export HTML result.
QString m_exportHtmlTemplate;
NoteState m_noteState; NoteState m_noteState;
ExportState m_state; ExportState m_state;

View File

@ -110,7 +110,7 @@ QString VNote::generateHtmlTemplate(const QString &p_renderBg,
cssStyle += "img { max-width: 100% !important; height: auto !important; }\n"; cssStyle += "img { max-width: 100% !important; height: auto !important; }\n";
} }
const QString styleHolder("<!-- BACKGROUND_PLACE_HOLDER -->"); const QString styleHolder("/* BACKGROUND_PLACE_HOLDER */");
const QString cssHolder("CSS_PLACE_HOLDER"); const QString cssHolder("CSS_PLACE_HOLDER");
const QString codeBlockCssHolder("HIGHLIGHTJS_CSS_PLACE_HOLDER"); const QString codeBlockCssHolder("HIGHLIGHTJS_CSS_PLACE_HOLDER");
@ -142,6 +142,32 @@ QString VNote::generateHtmlTemplate(const QString &p_renderBg,
return templ; return templ;
} }
QString VNote::generateExportHtmlTemplate(const QString &p_renderBg)
{
const QString c_exportTemplatePath(":/resources/export_template.html");
QString cssStyle;
if (!p_renderBg.isEmpty()) {
cssStyle += "body { background-color: " + p_renderBg + " !important; }\n";
}
if (g_config->getEnableImageConstraint()) {
// Constain the image width.
cssStyle += "img { max-width: 100% !important; height: auto !important; }\n";
}
const QString styleHolder("/* BACKGROUND_PLACE_HOLDER */");
QString templ = VUtils::readFileFromDisk(c_exportTemplatePath);
g_palette->fillStyle(templ);
if (!cssStyle.isEmpty()) {
templ.replace(styleHolder, cssStyle);
}
return templ;
}
void VNote::updateTemplate() void VNote::updateTemplate()
{ {
QString renderBg = g_config->getRenderBackgroundColor(g_config->getCurRenderBackgroundColor()); QString renderBg = g_config->getRenderBackgroundColor(g_config->getCurRenderBackgroundColor());

View File

@ -105,6 +105,9 @@ public:
const QString &p_codeBlockStyleUrl, const QString &p_codeBlockStyleUrl,
bool p_isPDF); bool p_isPDF);
// @p_renderBg: background color, empty to not specify given color.
static QString generateExportHtmlTemplate(const QString &p_renderBg);
public slots: public slots:
void updateTemplate(); void updateTemplate();

View File

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