Export: support outline panel in exported HTML file

This commit is contained in:
Le Tan 2018-09-20 19:22:53 +08:00
parent 03ff391948
commit 77dd8d0f32
15 changed files with 515 additions and 217 deletions

View File

@ -372,11 +372,16 @@ QWidget *VExportDialog::setupHTMLAdvancedSettings()
m_completeHTMLCB->setEnabled(!checked);
});
QFormLayout *advLayout = new QFormLayout();
advLayout->addRow(m_embedStyleCB);
advLayout->addRow(m_completeHTMLCB);
advLayout->addRow(m_embedImagesCB);
advLayout->addRow(m_mimeHTMLCB);
// Outline panel.
m_outlinePanelCB = new QCheckBox(tr("Enable outline panel"), this);
m_outlinePanelCB->setToolTip(tr("Add an outline panel in HTML file"));
QGridLayout *advLayout = new QGridLayout();
advLayout->addWidget(m_embedStyleCB, 0, 1, 1, 2);
advLayout->addWidget(m_completeHTMLCB, 0, 4, 1, 2);
advLayout->addWidget(m_embedImagesCB, 1, 1, 1, 2);
advLayout->addWidget(m_mimeHTMLCB, 1, 4, 1, 2);
advLayout->addWidget(m_outlinePanelCB, 2, 1, 1, 2);
advLayout->setContentsMargins(0, 0, 0, 0);
@ -483,6 +488,8 @@ void VExportDialog::initUIFields(MarkdownConverterType p_renderer)
m_mimeHTMLCB->setChecked(s_opt.m_htmlOpt.m_mimeHTML);
m_outlinePanelCB->setChecked(s_opt.m_htmlOpt.m_outlinePanel);
m_tableOfContentsCB->setChecked(s_opt.m_pdfOpt.m_enableTableOfContents);
m_wkhtmltopdfCB->setChecked(s_opt.m_pdfOpt.m_wkhtmltopdf);
@ -587,7 +594,8 @@ void VExportDialog::startExport()
ExportHTMLOption(m_embedStyleCB->isChecked(),
m_completeHTMLCB->isChecked(),
m_embedImagesCB->isChecked(),
m_mimeHTMLCB->isChecked()),
m_mimeHTMLCB->isChecked(),
m_outlinePanelCB->isChecked()),
ExportCustomOption((ExportCustomOption::SourceFormat)
m_customSrcFormatCB->currentData().toInt(),
m_customSuffixEdit->text(),

View File

@ -58,18 +58,21 @@ struct ExportHTMLOption
: m_embedCssStyle(true),
m_completeHTML(true),
m_embedImages(true),
m_mimeHTML(false)
m_mimeHTML(false),
m_outlinePanel(true)
{
}
ExportHTMLOption(bool p_embedCssStyle,
bool p_completeHTML,
bool p_embedImages,
bool p_mimeHTML)
bool p_mimeHTML,
bool p_outlinePanel)
: m_embedCssStyle(p_embedCssStyle),
m_completeHTML(p_completeHTML),
m_embedImages(p_embedImages),
m_mimeHTML(p_mimeHTML)
m_mimeHTML(p_mimeHTML),
m_outlinePanel(p_outlinePanel)
{
}
@ -77,6 +80,7 @@ struct ExportHTMLOption
bool m_completeHTML;
bool m_embedImages;
bool m_mimeHTML;
bool m_outlinePanel;
};
@ -452,6 +456,8 @@ private:
QCheckBox *m_mimeHTMLCB;
QCheckBox *m_outlinePanelCB;
QCheckBox *m_subfolderCB;
QComboBox *m_customSrcFormatCB;

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<head>
<style type="text/css">
/* STYLE_GLOBAL_PLACE_HOLDER */
</style>
<style type="text/css">
/* STYLE_OUTLINE_PLACE_HOLDER */
/* STYLE_PLACE_HOLDER */
</style>
<!-- EXTRA_PLACE_HOLDER -->
<!-- HEAD_PLACE_HOLDER -->
</head>
<body>
<div class="container-fluid">
<div class="row flex-xl-nowrap">
<div id="outline-panel" style="display:none;" class="d-none d-md-block d-xl-block col-md-3 col-xl-2 bd-toc">
<div id="outline-content" class="section-nav"></div>
</div>
<div id="post-content" class="col-12 col-md-9 col-xl-10 py-md-3 pl-md-5 bd-content">
<!-- BODY_PLACE_HOLDER -->
</div>
</div>
</div>
<div id="container-floating" style="display:none;" class="d-none d-md-block d-xl-block">
<div id="floating-button" onclick="toggleMore()">
<p id="floating-more" class="more">&gt;</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,199 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
.container-fluid {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto {
position: relative;
width: 100%;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
}
.col-12 {
-webkit-box-flex: 0;
-ms-flex: 0 0 100%;
flex: 0 0 100%;
max-width: 100%;
}
@media (min-width: 768px) {
.col-md-3 {
-webkit-box-flex: 0;
-ms-flex: 0 0 25%;
flex: 0 0 25%;
max-width: 25%;
}
}
@media (min-width: 768px) {
.col-md-9 {
-webkit-box-flex: 0;
-ms-flex: 0 0 75%;
flex: 0 0 75%;
max-width: 75%;
}
}
@media (min-width: 1200px) {
.col-xl-2 {
-webkit-box-flex: 0;
-ms-flex: 0 0 16.666667%;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
}
@media (min-width: 1200px) {
.col-xl-10 {
-webkit-box-flex: 0;
-ms-flex: 0 0 83.333333%;
flex: 0 0 83.333333%;
max-width: 83.333333%;
}
}
@media (min-width: 768px) {
.pt-md-3, .py-md-3 {
padding-top: 1rem!important;
}
}
@media (min-width: 768px) {
.pb-md-3, .py-md-3 {
padding-bottom: 1rem!important;
}
}
@media (min-width: 768px) {
.pl-md-5, .px-md-5 {
padding-left: 3rem!important;
}
}
.d-none {
display: none!important;
}
@media (min-width: 1200px) {
.d-xl-block {
display: block!important;
}
}
@media (min-width: 768px) {
.d-md-block {
display: block!important;
}
}
.bd-content {
-webkit-box-ordinal-group: 1;
-ms-flex-order: 0;
order: 0;
}
.bd-toc {
position: -webkit-sticky;
position: sticky;
top: 4rem;
height: calc(100vh - 10rem);
overflow-y: auto;
}
.bd-toc {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
padding-top: 1.5rem;
padding-bottom: 1.5rem;
font-size: .875rem;
}
.section-nav {
padding-left: 0;
}
.section-nav ul {
font-size: .875rem;
list-style-type: none;
}
.section-nav li {
font-size: .875rem;
}
.section-nav a {
color: inherit !important;
}
.row {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
@media (min-width: 1200px) {
.flex-xl-nowrap {
flex-wrap: nowrap !important;
}
}
#floating-button {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: #00897B;
position: fixed;
top: .5rem;
right: .5rem;
cursor: pointer;
box-shadow: 0px 2px 5px #666;
}
#floating-button .more {
color: #F5F5F5;
position: absolute;
top: 0;
display: block;
bottom: 0;
left: 0;
right: 0;
text-align: center;
padding: 0;
margin: 0;
line-height: 2.5rem;
font-size: 2rem;
font-family: 'monospace';
font-weight: 300;
}
.hide-none {
display: none !important;
}
.col-expand {
-webkit-box-flex: 0;
-ms-flex: 0 0 100% !important;
flex: 0 0 100% !important;
max-width: 100% !important;
padding-right: 3rem !important;
}
.outline-bold {
font-weight: bolder !important;
}

View File

@ -0,0 +1,228 @@
var toc = [];
var setVisible = function(node, visible) {
var cl = 'hide-none';
if (visible) {
node.classList.remove(cl);
} else {
node.classList.add(cl);
}
};
var isVisible = function(node) {
var cl = 'hide-none';
return !node.classList.contains(cl);
};
var setPostContentExpanded = function(node, expanded) {
var cl = 'col-expand';
if (expanded) {
node.classList.add(cl);
} else {
node.classList.remove(cl);
}
};
var setOutlinePanelVisible = function(visible) {
var outlinePanel = document.getElementById('outline-panel');
var postContent = document.getElementById('post-content');
setVisible(outlinePanel, visible);
setPostContentExpanded(postContent, !visible);
};
var isOutlinePanelVisible = function() {
var outlinePanel = document.getElementById('outline-panel');
return isVisible(outlinePanel);
};
window.addEventListener('load', function() {
var outlinePanel = document.getElementById('outline-panel');
outlinePanel.style.display = 'initial';
var floatingContainer = document.getElementById('container-floating');
floatingContainer.style.display = 'initial';
var outlineContent = document.getElementById('outline-content');
var postContent = document.getElementById('post-content');
// Fetch the outline.
var headers = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");
toc = [];
for (var i = 0; i < headers.length; ++i) {
var header = headers[i];
toc.push({
level: parseInt(header.tagName.substr(1)),
anchor: header.id,
title: header.textContent
});
}
if (toc.length == 0) {
setOutlinePanelVisible(false);
setVisible(floatingContainer, false);
return;
}
var baseLevel = baseLevelOfToc(toc);
var tocTree = tocToTree(toPerfectToc(toc, baseLevel), baseLevel);
outlineContent.innerHTML = tocTree;
setOutlinePanelVisible(true);
setVisible(floatingContainer, true);
});
// Return the topest level of @toc, starting from 1.
var baseLevelOfToc = function(p_toc) {
var level = -1;
for (i in p_toc) {
if (level == -1) {
level = p_toc[i].level;
} else if (level > p_toc[i].level) {
level = p_toc[i].level;
}
}
if (level == -1) {
level = 1;
}
return level;
};
// Handle wrong title levels, such as '#' followed by '###'
var toPerfectToc = function(p_toc, p_baseLevel) {
var i;
var curLevel = p_baseLevel - 1;
var perfToc = [];
for (i in p_toc) {
var item = p_toc[i];
// Insert empty header.
while (item.level > curLevel + 1) {
curLevel += 1;
var tmp = { level: curLevel,
anchor: '',
title: '[EMPTY]'
};
perfToc.push(tmp);
}
perfToc.push(item);
curLevel = item.level;
}
return perfToc;
};
var itemToHtml = function(item) {
return '<a href="#' + item.anchor + '" data="' + item.anchor + '">' + item.title + '</a>';
};
// Turn a perfect toc to a tree using <ul>
var tocToTree = function(p_toc, p_baseLevel) {
var i;
var front = '<li>';
var ending = ['</li>'];
var curLevel = p_baseLevel;
for (i in p_toc) {
var item = p_toc[i];
if (item.level == curLevel) {
front += '</li>';
front += '<li>';
front += itemToHtml(item);
} else if (item.level > curLevel) {
// assert(item.level - curLevel == 1)
front += '<ul>';
ending.push('</ul>');
front += '<li>';
front += itemToHtml(item);
ending.push('</li>');
curLevel = item.level;
} else {
while (item.level < curLevel) {
var ele = ending.pop();
front += ele;
if (ele == '</ul>') {
curLevel--;
}
}
front += '</li>';
front += '<li>';
front += itemToHtml(item);
}
}
while (ending.length > 0) {
front += ending.pop();
}
front = front.replace("<li></li>", "");
front = '<ul>' + front + '</ul>';
return front;
};
var toggleMore = function() {
if (toc.length == 0) {
return;
}
var p = document.getElementById('floating-more');
if (isOutlinePanelVisible()) {
p.textContent = '<';
setOutlinePanelVisible(false);
} else {
p.textContent = '>';
setOutlinePanelVisible(true);
}
};
window.addEventListener('scroll', function() {
if (toc.length == 0 || !isOutlinePanelVisible()) {
return;
}
var postContent = document.getElementById('post-content');
var scrollTop = document.documentElement.scrollTop
|| document.body.scrollTop
|| window.pageYOffset;
var eles = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");
if (eles.length == 0) {
return;
}
var idx = -1;
var biaScrollTop = scrollTop + 50;
for (var i = 0; i < eles.length; ++i) {
if (biaScrollTop >= eles[i].offsetTop) {
idx = i;
} else {
break;
}
}
var header = '';
if (idx != -1) {
header = eles[idx].id;
}
highlightItemOnlyInOutline(header);
});
var highlightItemOnlyInOutline = function(id) {
var cl = 'outline-bold';
var outlineContent = document.getElementById('outline-content');
var eles = outlineContent.querySelectorAll("a");
var target = null;
for (var i = 0; i < eles.length; ++i) {
var ele = eles[i];
if (ele.getAttribute('data') == id) {
target = ele;
ele.classList.add(cl);
} else {
ele.classList.remove(cl);
}
}
// TODO: scroll target into view within the outline panel scroll area.
};

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<head>
<style type="text/css">
/* STYLE_GLOBAL_PLACE_HOLDER */
</style>
<style type="text/css">
/* STYLE_PLACE_HOLDER */
</style>
<!-- EXTRA_PLACE_HOLDER -->
<!-- HEAD_PLACE_HOLDER -->
</head>
<body>
<!-- BODY_PLACE_HOLDER -->
</body>
</html>

View File

@ -1,56 +0,0 @@
.post-content {
width: 960px;
min-height: 200px;
margin-left: auto;
margin-right: auto;
}
.toc {
overflow: hidden;
color: #555;
border: 1px solid #d2d2d2;
border-radius: 3px;
min-width: 150px;
opacity: 1;
font-size: inherit;
z-index: 19941112;
}
.toc {
display: none;
margin-bottom: 2em;
line-height: 1.5em;
}
.toc a {
color: #333;
}
.toc a:hover{
color: #555;
background-color: #fff;
}
.toc .catalog-title {
cursor: move;
padding-left: 12px;
width: 100%;
height: 35px;
line-height: 36px;
border-bottom: 1px solid #eee;
font-size: 14px;
color: #555;
overflow: hidden;
}
.toc .catalog-close {
position: absolute;
right: 15px;
top: 6px;
cursor: pointer;
text-decoration: none;
}
.fixed {
position: fixed;
top: 20px;
right: 250px;
width: auto;
}
.blodtoc {
font-weight: bold;
}

View File

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<head>
<style type="text/css">
/* STYLE_GLOBAL_PLACE_HOLDER */
</style>
<style type="text/css">
/* STYLE_PLACE_HOLDER */
</style>
<!-- EXTRA_PLACE_HOLDER -->
<!-- HEAD_PLACE_HOLDER -->
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<div class="toc fixed">
<div class="catalog-title"><span>目录</span></div>
<a class="catalog-close"><span>X</span></a>
</div>
<div class="post-content">
<!-- BODY_PLACE_HOLDER -->
</div>
</body>
</html>

View File

@ -1,100 +0,0 @@
/**
* Convert item of header array to html in li
* @param item An element of header array
*/
var itemToHtml = function(item) {
return '<a href="#' + item.id + '" id="' + 'menu-'+ item.id + '" >' + item.innerText + '</a>';
};
/**
* Generate tree from header array
* @param toc_list An array containing header elements
* @param p_baseLevel The base level number of the toc you want to display
*/
var tocToTree = function(toc_list, p_baseLevel) {
let i;
let p_toc = [];
for (i in toc_list) {
let itemLevel = parseInt(toc_list[i].tagName.substring(1));
if (itemLevel >= p_baseLevel) {
p_toc.push(toc_list[i]);
}
}
let front = '<li>';
let ending = ['</li>'];
let curLevel = p_baseLevel;
let toclen = p_toc.length;
for (i in p_toc) {
let item = p_toc[i];
console.log(item.tagName);
let itemLevel = parseInt(item.tagName.substring(1));
if (item.tagName == curLevel) {
front += '</li>';
front += '<li>';
front += itemToHtml(item);
} else if (itemLevel > curLevel) {
// assert(item.level - curLevel == 1)
front += '<ul>';
ending.push('</ul>');
front += '<li>';
front += itemToHtml(item);
ending.push('</li>');
curLevel = itemLevel;
} else {
while (itemLevel < curLevel) {
let ele = ending.pop();
front += ele;
if (ele == '</ul>') {
curLevel--;
}
}
front += '</li>';
front += '<li>';
front += itemToHtml(item);
}
}
while (ending.length > 0) {
front += ending.pop();
}
front = front.replace("<li></li>", "");
front = '<ul>' + front + '</ul>';
return front;
};
let headerObjList = $(":header").toArray();
$('.toc').append(tocToTree( headerObjList, 2 ));
// scroll to display side outline
$(window).bind('scroll', function(){
if ($(document).scrollTop() >= 100) {
$('.toc').css("display", "block");
highToc();
} else {
$('.toc').css("display", "none");
}
});
// make the corresponding outline text blod
let highToc = function(){
$(":header").each(function(index, element) {
var wst = $(window).scrollTop();
let tag_id = $(this).attr("id");
if($("#"+tag_id).offset().top <= wst){
$('.toc a').removeClass("blodtoc");
$('#menu-'+tag_id).addClass("blodtoc");
}
});
}
// click to make outline text blod
$('.toc a').click(function(){
$('.toc a').removeClass("blodtoc");
$(this).addClass("blodtoc");
});
// button to close the outline
$('.toc .catalog-close').click(function(){
$('.toc').hide();
$(window).unbind('scroll');
});

View File

@ -868,7 +868,9 @@ QString VUtils::generateHtmlTemplate(const QString &p_template,
return htmlTemplate;
}
QString VUtils::generateExportHtmlTemplate(const QString &p_renderBg, bool p_includeMathJax)
QString VUtils::generateExportHtmlTemplate(const QString &p_renderBg,
bool p_includeMathJax,
bool p_outlinePanel)
{
QString templ = VNote::generateExportHtmlTemplate(g_config->getRenderBackgroundColor(p_renderBg));
QString extra;
@ -902,6 +904,18 @@ with 2em, if there are Chinese characters in it, the font will be a mess.
extra += "<script type=\"text/javascript\" async src=\"" + mj + "\"></script>\n";
}
if (p_outlinePanel) {
const QString outlineCss(":/resources/export/outline.css");
QString css = VUtils::readFileFromDisk(outlineCss);
if (!css.isEmpty()) {
templ.replace(HtmlHolder::c_outlineStyleHolder, css);
}
const QString outlineJs(":/resources/export/outline.js");
QString js = VUtils::readFileFromDisk(outlineJs);
extra += QString("<script type=\"text/javascript\">\n%1\n</script>\n").arg(js);
}
if (!extra.isEmpty()) {
templ.replace(HtmlHolder::c_extraHolder, extra);
}

View File

@ -195,7 +195,9 @@ public:
bool p_addToc = false);
// @p_renderBg is the background name.
static QString generateExportHtmlTemplate(const QString &p_renderBg, bool p_includeMathJax);
static QString generateExportHtmlTemplate(const QString &p_renderBg,
bool p_includeMathJax,
bool p_outlinePanel);
static QString generateSimpleHtmlTemplate(const QString &p_body);

View File

@ -48,6 +48,7 @@ namespace HtmlHolder
static const QString c_bodyHolder = "<!-- BODY_PLACE_HOLDER -->";
static const QString c_headHolder = "<!-- HEAD_PLACE_HOLDER -->";
static const QString c_styleHolder = "/* STYLE_PLACE_HOLDER */";
static const QString c_outlineStyleHolder = "/* STYLE_OUTLINE_PLACE_HOLDER */";
}
// Directory Config file items.

View File

@ -55,8 +55,13 @@ void VExporter::prepareExport(const ExportOption &p_opt)
isPdf && p_opt.m_pdfOpt.m_wkhtmltopdf,
extraToc);
bool outline = p_opt.m_htmlOpt.m_outlinePanel
&& !isPdf
&& (p_opt.m_format == ExportFormat::HTML
|| p_opt.m_format == ExportFormat::Custom);
m_exportHtmlTemplate = VUtils::generateExportHtmlTemplate(p_opt.m_renderBg,
isPdf && p_opt.m_pdfOpt.m_wkhtmltopdf);
isPdf && p_opt.m_pdfOpt.m_wkhtmltopdf,
outline);
m_pageLayout = *(p_opt.m_pdfOpt.m_layout);

View File

@ -152,7 +152,7 @@ QString VNote::generateHtmlTemplate(const QString &p_renderBg,
QString VNote::generateExportHtmlTemplate(const QString &p_renderBg)
{
const QString c_exportTemplatePath(":/resources/export_template.html");
const QString c_exportTemplatePath(":/resources/export/export_template.html");
QString cssStyle;
if (!p_renderBg.isEmpty()) {

View File

@ -177,7 +177,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>
<file>resources/themes/v_pure/v_pure_mermaid.css</file>
<file>resources/themes/v_moonlight/v_moonlight_mermaid.css</file>
<file>resources/themes/v_moonlight/arrow_dropdown_disabled.svg</file>
@ -272,5 +271,8 @@
<file>resources/common.css</file>
<file>resources/icons/quick_access.svg</file>
<file>resources/common.js</file>
<file>resources/export/export_template.html</file>
<file>resources/export/outline.css</file>
<file>resources/export/outline.js</file>
</qresource>
</RCC>