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

View File

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

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 html = mdit.render(text);
content.highlightTextCB(html, id, timeStamp);
}
};
var textToHtml = function(text) {
var html = mdit.render(text);
@ -139,4 +139,4 @@ var textToHtml = function(text) {
container.innerHTML = "";
content.textToHtmlCB(text, html);
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,9 @@ void VExporter::prepareExport(const ExportOption &p_opt)
p_opt.m_renderStyle,
p_opt.m_renderCodeBlockStyle,
p_opt.m_format == ExportFormat::PDF);
m_exportHtmlTemplate = VUtils::generateExportHtmlTemplate(p_opt.m_renderBg);
m_pageLayout = *(p_opt.m_layout);
}
@ -37,70 +40,15 @@ bool VExporter::exportPDF(VFile *p_file,
const QString &p_outputFile,
QString *p_errMsg)
{
Q_UNUSED(p_errMsg);
return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg);
}
bool ret = false;
bool isOpened = p_file->isOpened();
if (!isOpened && !p_file->open()) {
goto exit;
}
Q_ASSERT(m_state == ExportState::Idle);
m_state = ExportState::Busy;
clearNoteState();
initWebViewer(p_file, p_opt);
while (!isNoteStateReady()) {
VUtils::sleepWait(100);
if (m_state == ExportState::Cancelled) {
goto exit;
}
if (isNoteStateFailed()) {
m_state = ExportState::Failed;
goto exit;
}
}
// Wait to ensure Web side is really ready.
VUtils::sleepWait(200);
if (m_state == ExportState::Cancelled) {
goto exit;
}
{
bool exportRet = exportToPDF(m_webViewer,
p_outputFile,
m_pageLayout);
clearNoteState();
if (!isOpened) {
p_file->close();
}
if (exportRet) {
m_state = ExportState::Successful;
} else {
m_state = ExportState::Failed;
}
}
exit:
clearWebViewer();
if (m_state == ExportState::Successful) {
ret = true;
}
m_state = ExportState::Idle;
return ret;
bool VExporter::exportHTML(VFile *p_file,
const ExportOption &p_opt,
const QString &p_outputFile,
QString *p_errMsg)
{
return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg);
}
void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt)
@ -116,12 +64,12 @@ void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt)
connect(page, &VPreviewPage::loadFinished,
this, &VExporter::handleLoadFinished);
VDocument *document = new VDocument(p_file, m_webViewer);
connect(document, &VDocument::logicsFinished,
m_webDocument = new VDocument(p_file, m_webViewer);
connect(m_webDocument, &VDocument::logicsFinished,
this, &VExporter::handleLogicsFinished);
QWebChannel *channel = new QWebChannel(m_webViewer);
channel->registerObject(QStringLiteral("content"), document);
channel->registerObject(QStringLiteral("content"), m_webDocument);
page->setWebChannel(channel);
// Need to generate HTML using Hoedown.
@ -131,7 +79,7 @@ void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt)
QString html = mdConverter.generateHtml(p_file->getContent(),
g_config->getMarkdownExtensions(),
toc);
document->setHtml(html);
m_webDocument->setHtml(html);
}
m_webViewer->setHtml(m_htmlTemplate, p_file->getBaseUrl());
@ -155,10 +103,10 @@ void VExporter::handleLoadFinished(bool p_ok)
void VExporter::clearWebViewer()
{
if (m_webViewer) {
delete m_webViewer;
m_webViewer = NULL;
}
// m_webDocument will be freeed by QObject.
delete m_webViewer;
m_webViewer = NULL;
m_webDocument = NULL;
}
bool VExporter::exportToPDF(VWebView *p_webViewer,
@ -198,3 +146,135 @@ bool VExporter::exportToPDF(VWebView *p_webViewer,
return pdfPrinted == 1;
}
bool VExporter::exportViaWebView(VFile *p_file,
const ExportOption &p_opt,
const QString &p_outputFile,
QString *p_errMsg)
{
Q_UNUSED(p_errMsg);
bool ret = false;
bool isOpened = p_file->isOpened();
if (!isOpened && !p_file->open()) {
goto exit;
}
Q_ASSERT(m_state == ExportState::Idle);
m_state = ExportState::Busy;
clearNoteState();
initWebViewer(p_file, p_opt);
while (!isNoteStateReady()) {
VUtils::sleepWait(100);
if (m_state == ExportState::Cancelled) {
goto exit;
}
if (isNoteStateFailed()) {
m_state = ExportState::Failed;
goto exit;
}
}
// Wait to ensure Web side is really ready.
VUtils::sleepWait(200);
if (m_state == ExportState::Cancelled) {
goto exit;
}
{
bool exportRet = false;
switch (p_opt.m_format) {
case ExportFormat::PDF:
exportRet = exportToPDF(m_webViewer,
p_outputFile,
m_pageLayout);
break;
case ExportFormat::HTML:
exportRet = exportToHTML(m_webViewer,
m_webDocument,
p_outputFile);
break;
default:
break;
}
clearNoteState();
if (!isOpened) {
p_file->close();
}
if (exportRet) {
m_state = ExportState::Successful;
} else {
m_state = ExportState::Failed;
}
}
exit:
clearWebViewer();
if (m_state == ExportState::Successful) {
ret = true;
}
m_state = ExportState::Idle;
return ret;
}
bool VExporter::exportToHTML(VWebView *p_webViewer,
VDocument *p_webDocument,
const QString &p_filePath)
{
Q_UNUSED(p_webViewer);
int htmlExported = 0;
connect(p_webDocument, &VDocument::htmlContentFinished,
this, [&, this](const QString &p_headContent, const QString &p_bodyContent) {
if (p_bodyContent.isEmpty() || this->m_state == ExportState::Cancelled) {
htmlExported = -1;
return;
}
Q_ASSERT(!p_filePath.isEmpty());
QFile file(p_filePath);
if (!file.open(QFile::WriteOnly)) {
htmlExported = -1;
return;
}
QString html(m_exportHtmlTemplate);
html.replace(HtmlHolder::c_headHolder, p_headContent);
html.replace(HtmlHolder::c_bodyHolder, p_bodyContent);
file.write(html.toUtf8());
file.close();
htmlExported = 1;
});
p_webDocument->getHtmlContentAsync();
while (htmlExported == 0) {
VUtils::sleepWait(100);
if (m_state == ExportState::Cancelled) {
break;
}
}
return htmlExported == 1;
}

View File

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

View File

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

View File

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

View File

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