support export

This commit is contained in:
Le Tan 2021-01-13 21:47:45 +08:00
parent 7595b03639
commit 04a57f4f8d
112 changed files with 3953 additions and 284 deletions

View File

@ -562,3 +562,8 @@ Buffer::StateFlags Buffer::state() const
{
return m_state;
}
QSharedPointer<File> Buffer::getFile() const
{
return m_provider->getFile();
}

View File

@ -18,6 +18,7 @@ namespace vnotex
class ViewWindow;
struct FileOpenParameters;
class BufferProvider;
class File;
struct BufferParameters
{
@ -83,6 +84,9 @@ namespace vnotex
// Get the base path to resolve resources.
QString getResourcePath() const;
// Return nullptr if not available.
QSharedPointer<File> getFile() const;
ID getID() const;
// Get buffer content.

View File

@ -74,6 +74,9 @@ namespace vnotex
virtual bool isReadOnly() const = 0;
// Return nullptr if not available.
virtual QSharedPointer<File> getFile() const = 0;
protected:
virtual QDateTime getLastModifiedFromFile() const;

View File

@ -178,3 +178,8 @@ bool FileBufferProvider::isReadOnly() const
{
return m_readOnly;
}
QSharedPointer<File> FileBufferProvider::getFile() const
{
return m_file;
}

View File

@ -65,6 +65,8 @@ namespace vnotex
bool isReadOnly() const Q_DECL_OVERRIDE;
QSharedPointer<File> getFile() const Q_DECL_OVERRIDE;
private:
QSharedPointer<File> m_file;

View File

@ -8,6 +8,16 @@
using namespace vnotex;
QString FileType::preferredSuffix() const
{
return m_suffixes.isEmpty() ? QString() : m_suffixes.first();
}
bool FileType::isMarkdown() const
{
return m_type == Type::Markdown;
}
FileTypeHelper::FileTypeHelper()
{
setupBuiltInTypes();
@ -21,7 +31,7 @@ void FileTypeHelper::setupBuiltInTypes()
{
{
FileType type;
type.m_type = Type::Markdown;
type.m_type = FileType::Markdown;
type.m_displayName = Buffer::tr("Markdown");
type.m_typeName = QStringLiteral("Markdown");
type.m_suffixes << QStringLiteral("md")
@ -33,7 +43,7 @@ void FileTypeHelper::setupBuiltInTypes()
{
FileType type;
type.m_type = Type::Text;
type.m_type = FileType::Text;
type.m_typeName = QStringLiteral("Text");
type.m_displayName = Buffer::tr("Text");
type.m_suffixes << QStringLiteral("txt") << QStringLiteral("text") << QStringLiteral("log");
@ -42,7 +52,7 @@ void FileTypeHelper::setupBuiltInTypes()
{
FileType type;
type.m_type = Type::Others;
type.m_type = FileType::Others;
type.m_typeName = QStringLiteral("Others");
type.m_displayName = Buffer::tr("Others");
m_fileTypes.push_back(type);
@ -62,10 +72,10 @@ const FileType &FileTypeHelper::getFileType(const QString &p_filePath) const
// Treat all unknown text files as plain text files.
if (FileUtils::isText(p_filePath)) {
return m_fileTypes[Type::Text];
return m_fileTypes[FileType::Text];
}
return m_fileTypes[Type::Others];
return m_fileTypes[FileType::Others];
}
const FileType &FileTypeHelper::getFileTypeBySuffix(const QString &p_suffix) const
@ -74,7 +84,7 @@ const FileType &FileTypeHelper::getFileTypeBySuffix(const QString &p_suffix) con
if (it != m_suffixTypeMap.end()) {
return m_fileTypes.at(it.value());
} else {
return m_fileTypes[Type::Others];
return m_fileTypes[FileType::Others];
}
}
@ -97,8 +107,9 @@ const QVector<FileType> &FileTypeHelper::getAllFileTypes() const
return m_fileTypes;
}
const FileType &FileTypeHelper::getFileType(Type p_type) const
const FileType &FileTypeHelper::getFileType(int p_type) const
{
Q_ASSERT(p_type < m_fileTypes.size());
return m_fileTypes[p_type];
}
@ -108,9 +119,9 @@ const FileTypeHelper &FileTypeHelper::getInst()
return helper;
}
bool FileTypeHelper::checkFileType(const QString &p_filePath, Type p_type) const
bool FileTypeHelper::checkFileType(const QString &p_filePath, int p_type) const
{
return getFileType(p_filePath).m_type == static_cast<int>(p_type);
return getFileType(p_filePath).m_type == p_type;
}
const FileType &FileTypeHelper::getFileTypeByName(const QString &p_typeName) const
@ -122,5 +133,5 @@ const FileType &FileTypeHelper::getFileTypeByName(const QString &p_typeName) con
}
Q_ASSERT(false);
return m_fileTypes[Type::Others];
return m_fileTypes[FileType::Others];
}

View File

@ -10,7 +10,15 @@ namespace vnotex
class FileType
{
public:
// FileTypeHelper::Type.
// There may be other types after Others.
enum Type
{
Markdown = 0,
Text,
Others
};
// Type.
int m_type = -1;
QString m_typeName;
@ -19,25 +27,17 @@ namespace vnotex
QStringList m_suffixes;
QString preferredSuffix() const
{
return m_suffixes.isEmpty() ? QString() : m_suffixes.first();
}
QString preferredSuffix() const;
bool isMarkdown() const;
};
class FileTypeHelper
{
public:
enum Type
{
Markdown = 0,
Text,
Others
};
const FileType &getFileType(const QString &p_filePath) const;
const FileType &getFileType(Type p_type) const;
const FileType &getFileType(int p_type) const;
const FileType &getFileTypeByName(const QString &p_typeName) const;
@ -45,7 +45,7 @@ namespace vnotex
const QVector<FileType> &getAllFileTypes() const;
bool checkFileType(const QString &p_filePath, Type p_type) const;
bool checkFileType(const QString &p_filePath, int p_type) const;
static const FileTypeHelper &getInst();

View File

@ -8,12 +8,13 @@
using namespace vnotex;
NodeBufferProvider::NodeBufferProvider(const QSharedPointer<Node> &p_node, QObject *p_parent)
NodeBufferProvider::NodeBufferProvider(const QSharedPointer<Node> &p_node,
const QSharedPointer<File> &p_file,
QObject *p_parent)
: BufferProvider(p_parent),
m_node(p_node),
m_nodeFile(p_node->getContentFile())
m_nodeFile(p_file)
{
Q_ASSERT(m_nodeFile);
}
Buffer::ProviderType NodeBufferProvider::getType() const
@ -156,3 +157,8 @@ bool NodeBufferProvider::isReadOnly() const
{
return m_node->isReadOnly();
}
QSharedPointer<File> NodeBufferProvider::getFile() const
{
return m_nodeFile;
}

View File

@ -15,7 +15,9 @@ namespace vnotex
{
Q_OBJECT
public:
NodeBufferProvider(const QSharedPointer<Node> &p_node, QObject *p_parent = nullptr);
NodeBufferProvider(const QSharedPointer<Node> &p_node,
const QSharedPointer<File> &p_file,
QObject *p_parent = nullptr);
Buffer::ProviderType getType() const Q_DECL_OVERRIDE;
@ -65,6 +67,8 @@ namespace vnotex
bool isReadOnly() const Q_DECL_OVERRIDE;
QSharedPointer<File> getFile() const Q_DECL_OVERRIDE;
private:
QSharedPointer<Node> m_node;

View File

@ -42,11 +42,11 @@ void BufferMgr::initBufferServer()
// Markdown.
auto markdownFactory = QSharedPointer<MarkdownBufferFactory>::create();
m_bufferServer->registerItem(helper.getFileType(FileTypeHelper::Markdown).m_typeName, markdownFactory);
m_bufferServer->registerItem(helper.getFileType(FileType::Markdown).m_typeName, markdownFactory);
// Text.
auto textFactory = QSharedPointer<TextBufferFactory>::create();
m_bufferServer->registerItem(helper.getFileType(FileTypeHelper::Text).m_typeName, textFactory);
m_bufferServer->registerItem(helper.getFileType(FileType::Text).m_typeName, textFactory);
}
void BufferMgr::open(Node *p_node, const QSharedPointer<FileOpenParameters> &p_paras)
@ -62,7 +62,9 @@ void BufferMgr::open(Node *p_node, const QSharedPointer<FileOpenParameters> &p_p
auto buffer = findBuffer(p_node);
if (!buffer) {
auto nodePath = p_node->fetchAbsolutePath();
auto fileType = FileTypeHelper::getInst().getFileType(nodePath).m_typeName;
auto nodeFile = p_node->getContentFile();
Q_ASSERT(nodeFile);
auto fileType = nodeFile->getContentType().m_typeName;
auto factory = m_bufferServer->getItem(fileType);
if (!factory) {
// No factory to open this file type.
@ -72,7 +74,7 @@ void BufferMgr::open(Node *p_node, const QSharedPointer<FileOpenParameters> &p_p
}
BufferParameters paras;
paras.m_provider.reset(new NodeBufferProvider(p_node->sharedFromThis()));
paras.m_provider.reset(new NodeBufferProvider(p_node->sharedFromThis(), nodeFile));
buffer = factory->createBuffer(paras, this);
addBuffer(buffer);
}
@ -114,7 +116,8 @@ void BufferMgr::open(const QString &p_filePath, const QSharedPointer<FileOpenPar
auto buffer = findBuffer(p_filePath);
if (!buffer) {
// Open it as external file.
auto fileType = FileTypeHelper::getInst().getFileType(p_filePath).m_typeName;
auto externalFile = QSharedPointer<ExternalFile>::create(p_filePath);
auto fileType = externalFile->getContentType().m_typeName;
auto factory = m_bufferServer->getItem(fileType);
if (!factory) {
// No factory to open this file type.
@ -124,7 +127,7 @@ void BufferMgr::open(const QString &p_filePath, const QSharedPointer<FileOpenPar
}
BufferParameters paras;
paras.m_provider.reset(new FileBufferProvider(QSharedPointer<ExternalFile>::create(p_filePath),
paras.m_provider.reset(new FileBufferProvider(externalFile,
p_paras->m_nodeAttachedTo,
p_paras->m_readOnly));
buffer = factory->createBuffer(paras, this);

View File

@ -24,7 +24,7 @@
using namespace vnotex;
#ifndef QT_NO_DEBUG
// #define VX_DEBUG_WEB
#define VX_DEBUG_WEB
#endif
const QString ConfigMgr::c_orgName = "VNote";
@ -320,6 +320,18 @@ QString ConfigMgr::getUserThemeFolder() const
return folderPath;
}
QString ConfigMgr::getAppWebStylesFolder() const
{
return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("web-styles"));
}
QString ConfigMgr::getUserWebStylesFolder() const
{
auto folderPath = PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("web-styles"));
QDir().mkpath(folderPath);
return folderPath;
}
QString ConfigMgr::getAppDocsFolder() const
{
return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("docs"));
@ -406,3 +418,18 @@ QString ConfigMgr::getApplicationDirPath()
{
return PathUtils::parentDirPath(getApplicationFilePath());
}
QString ConfigMgr::getDocumentOrHomePath()
{
static QString docHomePath;
if (docHomePath.isEmpty()) {
QStringList folders = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);
if (folders.isEmpty()) {
docHomePath = QDir::homePath();
} else {
docHomePath = folders[0];
}
}
return docHomePath;
}

View File

@ -77,6 +77,10 @@ namespace vnotex
QString getUserThemeFolder() const;
QString getAppWebStylesFolder() const;
QString getUserWebStylesFolder() const;
QString getAppDocsFolder() const;
QString getUserDocsFolder() const;
@ -98,6 +102,8 @@ namespace vnotex
static QString getApplicationDirPath();
static QString getDocumentOrHomePath();
static const QString c_orgName;
static const QString c_appName;

View File

@ -32,7 +32,6 @@ SOURCES += \
$$PWD/widgetconfig.cpp
HEADERS += \
$$PWD/ViewerResource.h \
$$PWD/buffermgr.h \
$$PWD/configmgr.h \
$$PWD/coreconfig.h \
@ -58,4 +57,5 @@ HEADERS += \
$$PWD/theme.h \
$$PWD/sessionconfig.h \
$$PWD/clipboarddata.h \
$$PWD/webresource.h \
$$PWD/widgetconfig.h

View File

@ -31,6 +31,7 @@ namespace vnotex
DistributeSplits,
RemoveSplitAndWorkspace,
NewWorkspace,
Export,
MaxShortcut
};
Q_ENUM(Shortcut)

View File

@ -8,6 +8,7 @@ using namespace vnotex;
ExternalFile::ExternalFile(const QString &p_filePath)
: c_filePath(p_filePath)
{
setContentType(FileTypeHelper::getInst().getFileType(c_filePath).m_type);
}
QString ExternalFile::read() const

View File

@ -7,7 +7,7 @@ const FileType &File::getContentType() const
return FileTypeHelper::getInst().getFileType(m_contentType);
}
void File::setContentType(FileTypeHelper::Type p_type)
void File::setContentType(int p_type)
{
m_contentType = p_type;
}

View File

@ -60,10 +60,10 @@ namespace vnotex
const FileType &getContentType() const;
protected:
void setContentType(FileTypeHelper::Type p_type);
void setContentType(int p_type);
private:
FileTypeHelper::Type m_contentType = FileTypeHelper::Others;
int m_contentType = FileType::Others;
};
}

View File

@ -7,6 +7,7 @@
#include <utils/utils.h>
#include <utils/fileutils.h>
#include <utils/pathutils.h>
#include <utils/htmlutils.h>
#include <core/thememgr.h>
#include <core/vnotex.h>
@ -14,6 +15,8 @@ using namespace vnotex;
HtmlTemplateHelper::Template HtmlTemplateHelper::s_markdownViewerTemplate;
static const QString c_globalStylesPlaceholder = "/* VX_GLOBAL_STYLES_PLACEHOLDER */";
QString WebGlobalOptions::toJavascriptObject() const
{
return QStringLiteral("window.vxOptions = {\n")
@ -26,21 +29,22 @@ QString WebGlobalOptions::toJavascriptObject() const
+ QString("linkifyEnabled: %1,\n").arg(Utils::boolToString(m_linkifyEnabled))
+ QString("indentFirstLineEnabled: %1,\n").arg(Utils::boolToString(m_indentFirstLineEnabled))
+ QString("sectionNumberEnabled: %1,\n").arg(Utils::boolToString(m_sectionNumberEnabled))
+ QString("transparentBackgroundEnabled: %1,\n").arg(Utils::boolToString(m_transparentBackgroundEnabled))
+ QString("scrollable: %1,\n").arg(Utils::boolToString(m_scrollable))
+ QString("bodyWidth: %1,\n").arg(m_bodyWidth)
+ QString("bodyHeight: %1,\n").arg(m_bodyHeight)
+ QString("transformSvgToPngEnabled: %1,\n").arg(Utils::boolToString(m_transformSvgToPngEnabled))
+ QString("mathJaxScale: %1,\n").arg(m_mathJaxScale)
+ QString("sectionNumberBaseLevel: %1\n").arg(m_sectionNumberBaseLevel)
+ QStringLiteral("}");
}
static bool isGlobalStyles(const ViewerResource::Resource &p_resource)
{
return p_resource.m_name == QStringLiteral("global_styles");
}
// Read "global_styles" from resource and fill the holder with the content.
static void fillGlobalStyles(QString &p_template, const ViewerResource &p_resource)
static void fillGlobalStyles(QString &p_template, const WebResource &p_resource, const QString &p_additionalStyles)
{
QString styles;
for (const auto &ele : p_resource.m_resources) {
if (isGlobalStyles(ele)) {
if (ele.isGlobal()) {
if (ele.m_enabled) {
for (const auto &style : ele.m_styles) {
// Read the style file content.
@ -52,9 +56,10 @@ static void fillGlobalStyles(QString &p_template, const ViewerResource &p_resour
}
}
styles += p_additionalStyles;
if (!styles.isEmpty()) {
p_template.replace(QStringLiteral("/* VX_GLOBAL_STYLES_PLACEHOLDER */"),
styles);
p_template.replace(c_globalStylesPlaceholder, styles);
}
}
@ -76,13 +81,11 @@ static QString fillScriptTag(const QString &p_scriptFile)
return QString("<script type=\"text/javascript\" src=\"%1\"></script>\n").arg(url.toString());
}
static void fillThemeStyles(QString &p_template)
static void fillThemeStyles(QString &p_template, const QString &p_webStyleSheetFile, const QString &p_highlightStyleSheetFile)
{
QString styles;
const auto &themeMgr = VNoteX::getInst().getThemeMgr();
styles += fillStyleTag(themeMgr.getFile(Theme::File::WebStyleSheet));
styles += fillStyleTag(themeMgr.getFile(Theme::File::HighlightStyleSheet));
styles += fillStyleTag(p_webStyleSheetFile);
styles += fillStyleTag(p_highlightStyleSheetFile);
if (!styles.isEmpty()) {
p_template.replace(QStringLiteral("<!-- VX_THEME_STYLES_PLACEHOLDER -->"),
@ -97,13 +100,13 @@ static void fillGlobalOptions(QString &p_template, const WebGlobalOptions &p_opt
}
// Read all other resources in @p_resource and fill the holder with proper resource path.
static void fillResources(QString &p_template, const ViewerResource &p_resource)
static void fillResources(QString &p_template, const WebResource &p_resource)
{
QString styles;
QString scripts;
for (const auto &ele : p_resource.m_resources) {
if (ele.m_enabled && !isGlobalStyles(ele)) {
if (ele.m_enabled && !ele.isGlobal()) {
// Styles.
for (const auto &style : ele.m_styles) {
auto styleFile = ConfigMgr::getInst().getUserOrAppFile(style);
@ -129,6 +132,36 @@ static void fillResources(QString &p_template, const ViewerResource &p_resource)
}
}
static void fillResourcesByContent(QString &p_template, const WebResource &p_resource)
{
QString styles;
QString scripts;
for (const auto &ele : p_resource.m_resources) {
if (ele.m_enabled && !ele.isGlobal()) {
// Styles.
for (const auto &style : ele.m_styles) {
auto styleFile = ConfigMgr::getInst().getUserOrAppFile(style);
styles += FileUtils::readTextFile(styleFile);
}
// Scripts.
for (const auto &script : ele.m_scripts) {
auto scriptFile = ConfigMgr::getInst().getUserOrAppFile(script);
scripts += FileUtils::readTextFile(scriptFile);
}
}
}
if (!styles.isEmpty()) {
p_template.replace(QStringLiteral("/* VX_STYLES_PLACEHOLDER */"), styles);
}
if (!scripts.isEmpty()) {
p_template.replace(QStringLiteral("/* VX_SCRIPTS_PLACEHOLDER */"), scripts);
}
}
const QString &HtmlTemplateHelper::getMarkdownViewerTemplate()
{
return s_markdownViewerTemplate.m_template;
@ -142,16 +175,30 @@ void HtmlTemplateHelper::updateMarkdownViewerTemplate(const MarkdownEditorConfig
s_markdownViewerTemplate.m_revision = p_config.revision();
const auto &themeMgr = VNoteX::getInst().getThemeMgr();
s_markdownViewerTemplate.m_template =
generateMarkdownViewerTemplate(p_config,
themeMgr.getFile(Theme::File::WebStyleSheet),
themeMgr.getFile(Theme::File::HighlightStyleSheet));
}
QString HtmlTemplateHelper::generateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config,
const QString &p_webStyleSheetFile,
const QString &p_highlightStyleSheetFile,
bool p_useTransparentBg,
bool p_scrollable,
int p_bodyWidth,
int p_bodyHeight,
bool p_transformSvgToPng,
qreal p_mathJaxScale)
{
const auto &viewerResource = p_config.getViewerResource();
const auto templateFile = ConfigMgr::getInst().getUserOrAppFile(viewerResource.m_template);
auto htmlTemplate = FileUtils::readTextFile(templateFile);
{
auto templateFile = ConfigMgr::getInst().getUserOrAppFile(viewerResource.m_template);
s_markdownViewerTemplate.m_template = FileUtils::readTextFile(templateFile);
}
fillGlobalStyles(htmlTemplate, viewerResource, "");
fillGlobalStyles(s_markdownViewerTemplate.m_template, viewerResource);
fillThemeStyles(s_markdownViewerTemplate.m_template);
fillThemeStyles(htmlTemplate, p_webStyleSheetFile, p_highlightStyleSheetFile);
{
WebGlobalOptions opts;
@ -165,8 +212,66 @@ void HtmlTemplateHelper::updateMarkdownViewerTemplate(const MarkdownEditorConfig
opts.m_autoBreakEnabled = p_config.getAutoBreakEnabled();
opts.m_linkifyEnabled = p_config.getLinkifyEnabled();
opts.m_indentFirstLineEnabled = p_config.getIndentFirstLineEnabled();
fillGlobalOptions(s_markdownViewerTemplate.m_template, opts);
opts.m_transparentBackgroundEnabled = p_useTransparentBg;
opts.m_scrollable = p_scrollable;
opts.m_bodyWidth = p_bodyWidth;
opts.m_bodyHeight = p_bodyHeight;
opts.m_transformSvgToPngEnabled = p_transformSvgToPng;
opts.m_mathJaxScale = p_mathJaxScale;
fillGlobalOptions(htmlTemplate, opts);
}
fillResources(s_markdownViewerTemplate.m_template, viewerResource);
fillResources(htmlTemplate, viewerResource);
return htmlTemplate;
}
QString HtmlTemplateHelper::generateExportTemplate(const MarkdownEditorConfig &p_config,
bool p_addOutlinePanel)
{
auto exportResource = p_config.getExportResource();
const auto templateFile = ConfigMgr::getInst().getUserOrAppFile(exportResource.m_template);
auto htmlTemplate = FileUtils::readTextFile(templateFile);
fillGlobalStyles(htmlTemplate, exportResource, "");
// Outline panel.
for (auto &ele : exportResource.m_resources) {
if (ele.m_name == QStringLiteral("outline")) {
ele.m_enabled = p_addOutlinePanel;
break;
}
}
fillResourcesByContent(htmlTemplate, exportResource);
return htmlTemplate;
}
void HtmlTemplateHelper::fillTitle(QString &p_template, const QString &p_title)
{
if (!p_title.isEmpty()) {
p_template.replace("<!-- VX_TITLE_PLACEHOLDER -->",
QString("<title>%1</title>").arg(HtmlUtils::escapeHtml(p_title)));
}
}
void HtmlTemplateHelper::fillStyleContent(QString &p_template, const QString &p_styles)
{
p_template.replace("/* VX_STYLES_CONTENT_PLACEHOLDER */", p_styles);
}
void HtmlTemplateHelper::fillHeadContent(QString &p_template, const QString &p_head)
{
p_template.replace("<!-- VX_HEAD_PLACEHOLDER -->", p_head);
}
void HtmlTemplateHelper::fillContent(QString &p_template, const QString &p_content)
{
p_template.replace("<!-- VX_CONTENT_PLACEHOLDER -->", p_content);
}
void HtmlTemplateHelper::fillBodyClassList(QString &p_template, const QString &p_classList)
{
p_template.replace("<!-- VX_BODY_CLASS_LIST_PLACEHOLDER -->", p_classList);
}

View File

@ -30,6 +30,24 @@ namespace vnotex
bool m_indentFirstLineEnabled = false;
// Force to use transparent background.
bool m_transparentBackgroundEnabled = false;
// Whether the content elements are scrollable. Like PDF, it is false.
bool m_scrollable = true;
int m_bodyWidth = -1;
int m_bodyHeight = -1;
// Whether transform inlie SVG to PNG.
// For wkhtmltopdf converter, it could not render some inline SVG correctly.
// This is just a hint not mandatory. For now, PlantUML and Graphviz needs this.
bool m_transformSvgToPngEnabled = false;
// wkhtmltopdf will make the MathJax formula too small.
qreal m_mathJaxScale = -1;
QString toJavascriptObject() const;
};
@ -42,6 +60,29 @@ namespace vnotex
static const QString &getMarkdownViewerTemplate();
static void updateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config);
static QString generateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config,
const QString &p_webStyleSheetFile,
const QString &p_highlightStyleSheetFile,
bool p_useTransparentBg = false,
bool p_scrollable = true,
int p_bodyWidth = -1,
int p_bodyHeight = -1,
bool p_transformSvgToPng = false,
qreal p_mathJaxScale = -1);
static QString generateExportTemplate(const MarkdownEditorConfig &p_config,
bool p_addOutlinePanel);
static void fillTitle(QString &p_template, const QString &p_title);
static void fillStyleContent(QString &p_template, const QString &p_styles);
static void fillHeadContent(QString &p_template, const QString &p_head);
static void fillContent(QString &p_template, const QString &p_content);
static void fillBodyClassList(QString &p_template, const QString &p_classList);
private:
struct Template
{

View File

@ -27,6 +27,7 @@ void MarkdownEditorConfig::init(const QJsonObject &p_app, const QJsonObject &p_u
const auto userObj = p_user.value(m_sessionName).toObject();
loadViewerResource(appObj, userObj);
loadExportResource(appObj, userObj);
m_webPlantUml = READBOOL(QStringLiteral("web_plantuml"));
m_webGraphviz = READBOOL(QStringLiteral("web_graphviz"));
@ -58,6 +59,7 @@ QJsonObject MarkdownEditorConfig::toJson() const
{
QJsonObject obj;
obj[QStringLiteral("viewer_resource")] = saveViewerResource();
obj[QStringLiteral("export_resource")] = saveExportResource();
obj[QStringLiteral("web_plantuml")] = m_webPlantUml;
obj[QStringLiteral("web_graphviz")] = m_webGraphviz;
obj[QStringLiteral("prepend_dot_in_relative_link")] = m_prependDotInRelativeLink;
@ -122,11 +124,41 @@ QJsonObject MarkdownEditorConfig::saveViewerResource() const
return m_viewerResource.toJson();
}
const ViewerResource &MarkdownEditorConfig::getViewerResource() const
void MarkdownEditorConfig::loadExportResource(const QJsonObject &p_app, const QJsonObject &p_user)
{
const QString name(QStringLiteral("export_resource"));
if (MainConfig::isVersionChanged()) {
bool needOverride = p_app[QStringLiteral("override_viewer_resource")].toBool();
if (needOverride) {
qInfo() << "override \"viewer_resource\" in user configuration due to version change";
m_exportResource.init(p_app[name].toObject());
return;
}
}
if (p_user.contains(name)) {
m_exportResource.init(p_user[name].toObject());
} else {
m_exportResource.init(p_app[name].toObject());
}
}
QJsonObject MarkdownEditorConfig::saveExportResource() const
{
return m_exportResource.toJson();
}
const WebResource &MarkdownEditorConfig::getViewerResource() const
{
return m_viewerResource;
}
const WebResource &MarkdownEditorConfig::getExportResource() const
{
return m_exportResource;
}
bool MarkdownEditorConfig::getWebPlantUml() const
{
return m_webPlantUml;

View File

@ -3,7 +3,7 @@
#include "iconfig.h"
#include "viewerresource.h"
#include "webresource.h"
#include <QSharedPointer>
#include <QVector>
@ -38,15 +38,14 @@ namespace vnotex
QJsonObject toJson() const Q_DECL_OVERRIDE;
void loadViewerResource(const QJsonObject &p_app, const QJsonObject &p_user);
QJsonObject saveViewerResource() const;
int revision() const Q_DECL_OVERRIDE;
TextEditorConfig &getTextEditorConfig();
const TextEditorConfig &getTextEditorConfig() const;
const ViewerResource &getViewerResource() const;
const WebResource &getViewerResource() const;
const WebResource &getExportResource() const;
bool getWebPlantUml() const;
@ -107,9 +106,17 @@ namespace vnotex
QString sectionNumberStyleToString(SectionNumberStyle p_style) const;
SectionNumberStyle stringToSectionNumberStyle(const QString &p_str) const;
void loadViewerResource(const QJsonObject &p_app, const QJsonObject &p_user);
QJsonObject saveViewerResource() const;
void loadExportResource(const QJsonObject &p_app, const QJsonObject &p_user);
QJsonObject saveExportResource() const;
QSharedPointer<TextEditorConfig> m_textEditorConfig;
ViewerResource m_viewerResource;
WebResource m_viewerResource;
WebResource m_exportResource;
// Whether use javascript or external program to render PlantUML.
bool m_webPlantUml = true;

View File

@ -263,6 +263,10 @@ QDir Node::toDir() const
void Node::load()
{
if (isLoaded()) {
return;
}
getConfigMgr()->loadNode(this);
}

View File

@ -15,6 +15,7 @@ VXNodeFile::VXNodeFile(const QSharedPointer<VXNode> &p_node)
: m_node(p_node)
{
Q_ASSERT(m_node && m_node->hasContent());
setContentType(FileTypeHelper::getInst().getFileType(getContentPath()).m_type);
}
QString VXNodeFile::read() const

View File

@ -1,5 +1,4 @@
SOURCES += \
$$PWD/nodecontentmediautils.cpp \
$$PWD/vxnotebookconfigmgr.cpp \
$$PWD/vxnotebookconfigmgrfactory.cpp \
$$PWD/inotebookconfigmgr.cpp \
@ -8,7 +7,6 @@ SOURCES += \
HEADERS += \
$$PWD/inotebookconfigmgr.h \
$$PWD/nodecontentmediautils.h \
$$PWD/vxnotebookconfigmgr.h \
$$PWD/inotebookconfigmgrfactory.h \
$$PWD/vxnotebookconfigmgrfactory.h \

View File

@ -14,7 +14,7 @@
#include <utils/pathutils.h>
#include <exception.h>
#include "nodecontentmediautils.h"
#include <utils/contentmediautils.h>
using namespace vnotex;
@ -581,13 +581,13 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFileNodeAsChildOf(const QSharedPoi
getBackend()->copyFile(srcFilePath, destFilePath);
// Copy media files fetched from content.
NodeContentMediaUtils::copyMediaFiles(p_src.data(), getBackend().data(), destFilePath);
ContentMediaUtils::copyMediaFiles(p_src.data(), getBackend().data(), destFilePath);
// Copy attachment folder. Rename attachment folder if conflicts.
QString attachmentFolder = p_src->getAttachmentFolder();
if (!attachmentFolder.isEmpty()) {
auto destAttachmentFolderPath = fetchNodeAttachmentFolder(destFilePath, attachmentFolder);
NodeContentMediaUtils::copyAttachment(p_src.data(), getBackend().data(), destFilePath, destAttachmentFolderPath);
ContentMediaUtils::copyAttachment(p_src.data(), getBackend().data(), destFilePath, destAttachmentFolderPath);
}
// Create a file node.
@ -690,7 +690,7 @@ void VXNotebookConfigMgr::removeFilesOfNode(Node *p_node, bool p_force)
}
// Delete media files fetched from content.
NodeContentMediaUtils::removeMediaFiles(p_node);
ContentMediaUtils::removeMediaFiles(p_node);
// Delete node file itself.
auto filePath = p_node->fetchPath();
@ -787,7 +787,7 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFileAsChildOf(const QString &p_src
getBackend()->copyFile(p_srcPath, destFilePath);
// Copy media files fetched from content.
NodeContentMediaUtils::copyMediaFiles(p_srcPath, getBackend().data(), destFilePath);
ContentMediaUtils::copyMediaFiles(p_srcPath, getBackend().data(), destFilePath);
// Create a file node.
auto currentTime = QDateTime::currentDateTimeUtc();

View File

@ -55,9 +55,13 @@ void SessionConfig::init()
loadCore(sessionJobj);
loadStateAndGeometry(sessionJobj);
if (MainConfig::isVersionChanged()) {
doVersionSpecificOverride();
}
m_exportOption.fromJson(sessionJobj[QStringLiteral("export_option")].toObject());
}
void SessionConfig::loadCore(const QJsonObject &p_session)
@ -172,6 +176,7 @@ QJsonObject SessionConfig::toJson() const
obj[QStringLiteral("core")] = saveCore();
obj[QStringLiteral("notebooks")] = saveNotebooks();
obj[QStringLiteral("state_geometry")] = saveStateAndGeometry();
obj[QStringLiteral("export_option")] = m_exportOption.toJson();
return obj;
}
@ -185,22 +190,12 @@ QJsonObject SessionConfig::saveStateAndGeometry() const
SessionConfig::MainWindowStateGeometry SessionConfig::getMainWindowStateGeometry() const
{
auto sessionSettings = getMgr()->getSettings(ConfigMgr::Source::Session);
const auto &sessionJobj = sessionSettings->getJson();
const auto obj = sessionJobj.value(QStringLiteral("state_geometry")).toObject();
MainWindowStateGeometry sg;
sg.m_mainState = readByteArray(obj, QStringLiteral("main_window_state"));
sg.m_mainGeometry = readByteArray(obj, QStringLiteral("main_window_geometry"));
return sg;
return m_mainWindowStateGeometry;
}
void SessionConfig::setMainWindowStateGeometry(const SessionConfig::MainWindowStateGeometry &p_state)
{
m_mainWindowStateGeometry = p_state;
++m_revision;
writeToSettings();
updateConfig(m_mainWindowStateGeometry, p_state, this);
}
SessionConfig::OpenGL SessionConfig::getOpenGLAtBootstrap()
@ -283,3 +278,20 @@ void SessionConfig::doVersionSpecificOverride()
// In a new version, we may want to change one value by force.
// SHOULD set the in memory variable only, or will override the notebook list.
}
const ExportOption &SessionConfig::getExportOption() const
{
return m_exportOption;
}
void SessionConfig::setExportOption(const ExportOption &p_option)
{
updateConfig(m_exportOption, p_option, this);
}
void SessionConfig::loadStateAndGeometry(const QJsonObject &p_session)
{
const auto obj = p_session.value(QStringLiteral("state_geometry")).toObject();
m_mainWindowStateGeometry.m_mainState = readByteArray(obj, QStringLiteral("main_window_state"));
m_mainWindowStateGeometry.m_mainGeometry = readByteArray(obj, QStringLiteral("main_window_geometry"));
}

View File

@ -6,6 +6,8 @@
#include <QString>
#include <QVector>
#include <export/exportdata.h>
namespace vnotex
{
class SessionConfig : public IConfig
@ -82,6 +84,9 @@ namespace vnotex
int getMinimizeToSystemTray() const;
void setMinimizeToSystemTray(bool p_enabled);
const ExportOption &getExportOption() const;
void setExportOption(const ExportOption &p_option);
private:
void loadCore(const QJsonObject &p_session);
@ -91,6 +96,8 @@ namespace vnotex
QJsonArray saveNotebooks() const;
void loadStateAndGeometry(const QJsonObject &p_session);
QJsonObject saveStateAndGeometry() const;
void doVersionSpecificOverride();
@ -102,8 +109,6 @@ namespace vnotex
QVector<SessionConfig::NotebookItem> m_notebooks;
// Used to store newly-set state and geometry, since there is no need to store the read-in
// data all the time.
MainWindowStateGeometry m_mainWindowStateGeometry;
OpenGL m_openGL = OpenGL::None;
@ -116,6 +121,8 @@ namespace vnotex
// 0 for disabling minimizing to system tray;
// 1 for enabling minimizing to system tray.
int m_minimizeToSystemTray = -1;
ExportOption m_exportOption;
};
} // ns vnotex

View File

@ -368,7 +368,12 @@ bool Theme::isRef(const QString &p_str)
QString Theme::getFile(File p_fileType) const
{
QDir dir(m_themeFolderPath);
return getFile(m_themeFolderPath, p_fileType);
}
QString Theme::getFile(const QString &p_themeFolder, File p_fileType)
{
QDir dir(p_themeFolder);
if (dir.exists(getFileName(p_fileType))) {
return dir.filePath(getFileName(p_fileType));
} else if (p_fileType == File::MarkdownEditorStyle) {

View File

@ -54,6 +54,8 @@ namespace vnotex
static QPixmap getCover(const QString &p_folder);
static QString getFile(const QString &p_themeFolder, File p_fileType);
private:
struct Metadata
{

View File

@ -15,6 +15,8 @@ using namespace vnotex;
QStringList ThemeMgr::s_searchPaths;
QStringList ThemeMgr::s_webStylesSearchPaths;
ThemeMgr::ThemeMgr(const QString &p_currentThemeName, QObject *p_parent)
: QObject(p_parent)
{
@ -203,3 +205,39 @@ void ThemeMgr::refresh()
{
loadAvailableThemes();
}
void ThemeMgr::addWebStylesSearchPath(const QString &p_path)
{
s_webStylesSearchPaths << p_path;
}
QVector<QPair<QString, QString>> ThemeMgr::getWebStyles() const
{
QVector<QPair<QString, QString>> styles;
// From themes.
for (const auto &th : m_themes) {
auto filePath = Theme::getFile(th.m_folderPath, Theme::File::WebStyleSheet);
if (!filePath.isEmpty()) {
styles.push_back(qMakePair(tr("[Theme] %1 %2").arg(th.m_displayName, PathUtils::fileName(filePath)),
filePath));
}
filePath = Theme::getFile(th.m_folderPath, Theme::File::HighlightStyleSheet);
if (!filePath.isEmpty()) {
styles.push_back(qMakePair(tr("[Theme] %1 %2").arg(th.m_displayName, PathUtils::fileName(filePath)),
filePath));
}
}
// From search paths.
for (const auto &pa : s_webStylesSearchPaths) {
QDir dir(pa);
auto styleFiles = dir.entryList({"*.css"}, QDir::Files);
for (const auto &file : styleFiles) {
styles.push_back(qMakePair(file, dir.filePath(file)));
}
}
return styles;
}

View File

@ -64,10 +64,16 @@ namespace vnotex
// Won't affect current theme since we do not support changing theme real time for now.
void refresh();
// Return all web stylesheets available, including those from themes and web styles search paths.
// <DisplayName, FilePath>.
QVector<QPair<QString, QString>> getWebStyles() const;
static void addSearchPath(const QString &p_path);
static void addSyntaxHighlightingSearchPaths(const QStringList &p_paths);
static void addWebStylesSearchPath(const QString &p_path);
private:
void loadAvailableThemes();
@ -89,8 +95,11 @@ namespace vnotex
// Set at runtime, not from the theme config.
QColor m_baseBackground;
// List of path to search for themes.
// List of paths to search for themes.
static QStringList s_searchPaths;
// List of paths to search for CSS styles, including CSS syntax highlighting styles.
static QStringList s_webStylesSearchPaths;
};
} // ns vnotex

View File

@ -46,6 +46,8 @@ void VNoteX::initThemeMgr()
ThemeMgr::addSyntaxHighlightingSearchPaths(
QStringList() << configMgr.getUserSyntaxHighlightingFolder()
<< configMgr.getAppSyntaxHighlightingFolder());
ThemeMgr::addWebStylesSearchPath(configMgr.getAppWebStylesFolder());
ThemeMgr::addWebStylesSearchPath(configMgr.getUserWebStylesFolder());
m_themeMgr = new ThemeMgr(configMgr.getCoreConfig().getTheme(), this);
}

View File

@ -97,6 +97,8 @@ namespace vnotex
// Requested to locate node in explorer.
void locateNodeRequested(Node *p_node);
void exportRequested();
private:
explicit VNoteX(QObject *p_parent = nullptr);

View File

@ -1,5 +1,5 @@
#ifndef VIEWERRESOURCE_H
#define VIEWERRESOURCE_H
#ifndef WEBRESOURCE_H
#define WEBRESOURCE_H
#include <QJsonObject>
#include <QJsonArray>
@ -8,8 +8,8 @@
namespace vnotex
{
// Resource for Web viewer.
struct ViewerResource
// Resource for Web.
struct WebResource
{
struct Resource
{
@ -51,6 +51,11 @@ namespace vnotex
return obj;
}
bool isGlobal() const
{
return m_name == QStringLiteral("global_styles");
}
QString m_name;
bool m_enabled = true;
@ -96,4 +101,4 @@ namespace vnotex
}
#endif // VIEWERRESOURCE_H
#endif // WEBRESOURCE_H

View File

@ -24,7 +24,8 @@
"MaximizeSplit" : "Ctrl+G, Shift+\\",
"DistributeSplits" : "Ctrl+G, =",
"RemoveSplitAndWorkspace" : "Ctrl+G, R",
"NewWorkspace" : "Ctrl+G, N"
"NewWorkspace" : "Ctrl+G, N",
"Export" : "Ctrl+G, T"
},
"toolbar_icon_size" : 16
},
@ -83,7 +84,7 @@
"markdown_editor" : {
"override_viewer_resource" : true,
"viewer_resource" : {
"template" : "web/markdownviewertemplate.html",
"template" : "web/markdown-viewer-template.html",
"resources" : [
{
"name" : "global_styles",
@ -217,6 +218,28 @@
}
]
},
"export_resource" : {
"template" : "web/markdown-export-template.html",
"resources" : [
{
"name" : "global_styles",
"enabled" : true,
"styles" : [
"web/css/exportglobalstyles.css"
]
},
{
"name" : "outline",
"enabled" : true,
"styles" : [
"web/css/outline.css"
],
"scripts" : [
"web/js/outline.js"
]
}
]
},
"//comment" : "Whether use javascript or external program to render PlantUML",
"web_plantuml" : true,
"//comment" : "Whether use javascript or external program to render Graphviz",

View File

@ -8,10 +8,13 @@
<file>docs/zh_CN/about_vnotex.txt</file>
<file>docs/zh_CN/shortcuts.md</file>
<file>docs/zh_CN/markdown_guide.md</file>
<file>web/markdownviewertemplate.html</file>
<file>web/markdown-viewer-template.html</file>
<file>web/markdown-export-template.html</file>
<file>web/css/globalstyles.css</file>
<file>web/css/markdownit.css</file>
<file>web/css/imageviewer.css</file>
<file>web/css/outline.css</file>
<file>web/css/exportglobalstyles.css</file>
<file>web/js/qwebchannel.js</file>
<file>web/js/eventemitter.js</file>
<file>web/js/utils.js</file>
@ -26,6 +29,7 @@
<file>web/js/imageviewer.js</file>
<file>web/js/easyaccess.js</file>
<file>web/js/crosscopy.js</file>
<file>web/js/outline.js</file>
<file>web/js/markdown-it/markdown-it-container.min.js</file>
<file>web/js/markdown-it/markdown-it-emoji.min.js</file>
<file>web/js/markdown-it/markdown-it-footnote.min.js</file>

View File

@ -20,15 +20,15 @@ QWidget[DialogCentralWidget="true"] {
/* All widgets */
*[State="info"] {
border: 2px solid @widgets#qwidget#info#border;
border: 1px solid @widgets#qwidget#info#border;
}
*[State="warning"] {
border: 2px solid @widgets#qwidget#warning#border;
border: 1px solid @widgets#qwidget#warning#border;
}
*[State="error"] {
border: 2px solid @widgets#qwidget#error#border;
border: 1px solid @widgets#qwidget#error#border;
}
/* QAbstractScrollArea */
@ -430,6 +430,14 @@ QLineEdit:disabled {
color: @widgets#qlineedit#disabled#fg;
}
/* QPlainTextEdit */
QPlainTextEdit[ConsoleTextEdit="true"] {
color: @widgets#qlineedit#fg;
background-color: @widgets#qlineedit#bg;
selection-color: @widgets#qlineedit#selection#fg;
selection-background-color: @widgets#qlineedit#selection#bg;
}
/* QTabWidget */
QTabWidget {
border: none;

View File

@ -44,7 +44,7 @@
"bg2_9" : "#919cd8",
"fg10" : "#b71c1c",
"fg11" : "#ab5683",
"fg12" : "#283593",
"fg12" : "#5768c4",
"fg13" : "#b42b1f",
"fg15_3" : "#4f5666",
"fg15_4" : "#60697c",

View File

@ -10,15 +10,15 @@
/* All widgets */
*[State="info"] {
border: 2px solid @base#info#fg;
border: 1px solid @base#info#fg;
}
*[State="warning"] {
border: 2px solid @base#warning#fg;
border: 1px solid @base#warning#fg;
}
*[State="error"] {
border: 2px solid @base#error#fg;
border: 1px solid @base#error#fg;
}
/* ToolBox */

View File

@ -33,14 +33,14 @@ pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selectio
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background-color: #1976d2;
color: #f1f1f1;
color: #f5f5f5;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background-color: #1976d2;
color: #f1f1f1;
color: #f5f5f5;
}
@media print {

View File

@ -20,15 +20,15 @@ QWidget[DialogCentralWidget="true"] {
/* All widgets */
*[State="info"] {
border: 2px solid @widgets#qwidget#info#border;
border: 1px solid @widgets#qwidget#info#border;
}
*[State="warning"] {
border: 2px solid @widgets#qwidget#warning#border;
border: 1px solid @widgets#qwidget#warning#border;
}
*[State="error"] {
border: 2px solid @widgets#qwidget#error#border;
border: 1px solid @widgets#qwidget#error#border;
}
/* QAbstractScrollArea */
@ -430,6 +430,14 @@ QLineEdit:disabled {
color: @widgets#qlineedit#disabled#fg;
}
/* QPlainTextEdit */
QPlainTextEdit[ConsoleTextEdit="true"] {
color: @widgets#qlineedit#fg;
background-color: @widgets#qlineedit#bg;
selection-color: @widgets#qlineedit#selection#fg;
selection-background-color: @widgets#qlineedit#selection#bg;
}
/* QTabWidget */
QTabWidget {
border: none;

View File

@ -20,7 +20,7 @@
"bg3_4" : "#dadada",
"bg3_41" : "#e0e0e0",
"bg3_5" : "#eaeaea",
"bg3_6" : "#f1f1f1",
"bg3_6" : "#f5f5f5",
"fg3_5" : "#222222",
"fg3_6" : "#646464",
"fg3_7" : "#7a7a7a",
@ -33,7 +33,7 @@
"bg2_7" : "#e5f3f1",
"fg10" : "#b71c1c",
"fg11" : "#ab5683",
"fg12" : "#283593",
"fg12" : "#007b6e",
"fg13" : "#b42b1f",
"fg15_3" : "#b0b0b0",
"fg15_4" : "#7a7a7a",

View File

@ -10,8 +10,8 @@
"font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New",
"font-size" : 12,
"text-color" : "#222222",
"background-color" : "#f1f1f1",
"selected-text-color" : "#f1f1f1",
"background-color" : "#f5f5f5",
"selected-text-color" : "#f5f5f5",
"selected-background-color" : "#1976d2"
},
"CursorLine" : {
@ -30,7 +30,7 @@
},
"IndicatorsBorder" : {
"text-color" : "#aaaaaa",
"background-color" : "#ededed"
"background-color" : "#f1f1f1"
},
"CurrentLineNumber" : {
"text-color" : "#222222"
@ -70,8 +70,8 @@
"font-family" : "冬青黑体, YaHei Consolas Hybrid, Microsoft YaHei, 微软雅黑, Microsoft YaHei UI, WenQuanYi Micro Hei, 文泉驿雅黑, Dengxian, 等线体, STXihei, 华文细黑, Liberation Sans, Droid Sans, NSimSun, 新宋体, SimSun, 宋体, Verdana, Helvetica, sans-serif, Tahoma, Arial, Geneva, Georgia, Times New Roman",
"font-size" : 12,
"text-color" : "#222222",
"background-color" : "#f1f1f1",
"selected-text-color" : "#f1f1f1",
"background-color" : "#f5f5f5",
"selected-text-color" : "#f5f5f5",
"selected-background-color" : "#1976d2"
}
},

View File

@ -4,7 +4,7 @@ body {
color: #222222;
line-height: 1.5;
padding: 15px;
background-color: #f1f1f1;
background-color: #f5f5f5;
font-size: 16px;
}
@ -202,7 +202,7 @@ div.vx-plantuml-graph {
::selection {
background-color: #1976d2;
color: #f1f1f1;
color: #f5f5f5;
}
::-webkit-scrollbar {

View File

@ -0,0 +1,3 @@
div.code-toolbar > div.toolbar {
display: none;
}

View File

@ -73,9 +73,13 @@
content: counter(section1) "." counter(section2) "." counter(section3) "." counter(section4) "." counter(section5) "." counter(section6) ". ";
}
#vx-content.vx-constrain-image-width img {
max-width: 100%;
height: auto;
#vx-content.vx-constrain-image-width img,
#vx-content.vx-constrain-image-width div.vx-plantuml-graph > svg,
#vx-content.vx-constrain-image-width div.vx-mermaid-graph,
#vx-content.vx-constrain-image-width div.vx-flowchartjs-graph,
#vx-content.vx-constrain-image-width div.vx-wavedrom-graph {
max-width: 100% !important;
height: auto !important;
}
/* Table of Contents */
@ -133,3 +137,23 @@
#vx-content.vx-indent-first-line p {
text-indent: 2em;
}
body.vx-transparent-background {
background-color: transparent !important;
}
#vx-content.vx-nonscrollable pre {
white-space: pre-wrap !important;
word-break: break-all !important;
overflow: hidden !important;
}
#vx-content.vx-nonscrollable pre code {
white-space: pre-wrap !important;
word-break: break-all !important;
}
#vx-content.vx-nonscrollable code,
#vx-content.vx-nonscrollable a {
word-break: break-all !important;
}

View File

@ -0,0 +1,205 @@
*,
*::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;
}
@media print {
#floating-button {
display: none !important;
}
}

View File

@ -19,7 +19,8 @@ class Graphviz extends GraphRenderer {
registerInternal() {
this.vnotex.on('basicMarkdownRendered', () => {
this.reset();
this.renderCodeNodes(this.vnotex.contentContainer, 'svg');
this.renderCodeNodes(this.vnotex.contentContainer,
window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg');
});
this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs);

View File

@ -240,10 +240,6 @@ class MarkdownIt extends VxWorker {
}
registerInternal() {
this.vnotex.on('ready', () => {
this.setConstrainImageWidthEnabled(window.vxOptions.constrainImageWidthEnabled);
this.setIndentFirstLineEnabled(window.vxOptions.indentFirstLineEnabled);
});
this.vnotex.on('markdownTextUpdated', (p_text) => {
this.render(this.vnotex.contentContainer,
p_text,
@ -251,24 +247,6 @@ class MarkdownIt extends VxWorker {
});
}
setConstrainImageWidthEnabled(p_enabled) {
let constrainClass = 'vx-constrain-image-width';
if (p_enabled) {
this.vnotex.contentContainer.classList.add(constrainClass);
} else {
this.vnotex.contentContainer.classList.remove(constrainClass);
}
}
setIndentFirstLineEnabled(p_enabled) {
let constrainClass = 'vx-indent-first-line';
if (p_enabled) {
this.vnotex.contentContainer.classList.add(constrainClass);
} else {
this.vnotex.contentContainer.classList.remove(constrainClass);
}
}
// Render Markdown @p_text to HTML in @p_node.
// @p_finishCbStr will be called after finishing loading new content nodes.
// This could prevent Mermaid Gantt from negative width error.

View File

@ -43,6 +43,10 @@ new QWebChannel(qt.webChannelTransport,
window.vnotex.findText(p_text, p_options);
});
adapter.contentRequested.connect(function() {
window.vnotex.saveContent();
});
console.log('QWebChannel has been set up');
if (window.vnotex.initialized) {
window.vnotex.kickOffMarkdown();

View File

@ -18,7 +18,8 @@ window.MathJax = {
},
svg: {
// Make SVG self-contained.
fontCache: 'local'
fontCache: 'local',
scale: window.vxOptions.mathJaxScale > 0 ? window.vxOptions.mathJaxScale : 1
}
};

View File

@ -0,0 +1,241 @@
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');
// Escape @text to Html.
var escapeHtml = function(text) {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// 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: escapeHtml(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

@ -19,7 +19,8 @@ class PlantUml extends GraphRenderer {
registerInternal() {
this.vnotex.on('basicMarkdownRendered', () => {
this.reset();
this.renderCodeNodes(this.vnotex.contentContainer, 'svg');
this.renderCodeNodes(this.vnotex.contentContainer,
window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg');
});
this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs);

View File

@ -113,4 +113,55 @@ class Utils {
static headingSequenceRegExp() {
return /^\d{1,3}(?:\.\d+)*\. /;
}
static fetchStyleContent() {
let styles = "";
for (let styleIdx = 0; styleIdx < document.styleSheets.length; ++styleIdx) {
let styleSheet = document.styleSheets[styleIdx];
if (styleSheet.cssRules) {
let baseUrl = null;
if (styleSheet.href) {
let scheme = Utils.getUrlScheme(styleSheet.href);
// We only translate local resources.
if (scheme === 'file' || scheme === 'qrc') {
baseUrl = styleSheet.href.substr(0, styleSheet.href.lastIndexOf('/'));
}
}
for (let ruleIdx = 0; ruleIdx < styleSheet.cssRules.length; ++ruleIdx) {
let css = styleSheet.cssRules[ruleIdx].cssText;
if (baseUrl) {
// Try to replace the url() with absolute path.
css = Utils.translateCssUrlToAbsolute(baseUrl, css);
}
styles = styles + css + "\n";
}
}
}
return styles;
}
static translateCssUrlToAbsolute(p_baseUrl, p_css) {
let replaceCssUrl = function(baseUrl, match, p1, offset, str) {
if (Utils.getUrlScheme(p1)) {
return match;
}
let url = baseUrl + '/' + p1;
return "url(\"" + url + "\");";
};
return p_css.replace(/\burl\(\"([^\"\)]+)\"\);/g, replaceCssUrl.bind(undefined, p_baseUrl));
}
static getUrlScheme(p_url) {
let idx = p_url.indexOf(':');
if (idx > -1) {
return p_url.substr(0, idx);
} else {
return null;
}
}
}

View File

@ -60,6 +60,18 @@ class VNoteX extends EventEmitter {
this.sectionNumberBaseLevel = 3;
}
this.setContentContainerOption('vx-constrain-image-width',
window.vxOptions.constrainImageWidthEnabled || !window.vxOptions.scrollable);
this.setContentContainerOption('vx-indent-first-line',
window.vxOptions.indentFirstLineEnabled);
this.setBodyOption('vx-transparent-background',
window.vxOptions.transparentBackgroundEnabled);
this.setContentContainerOption('vx-nonscrollable',
!window.vxOptions.scrollable);
this.setBodySize(window.vxOptions.bodyWidth, window.vxOptions.bodyHeight);
document.body.style.height = '800';
this.initialized = true;
// Signal out.
@ -68,6 +80,22 @@ class VNoteX extends EventEmitter {
});
}
setContentContainerOption(p_class, p_enabled) {
if (p_enabled) {
this.contentContainer.classList.add(p_class);
} else {
this.contentContainer.classList.remove(p_class);
}
}
setBodyOption(p_class, p_enabled) {
if (p_enabled) {
document.body.classList.add(p_class);
} else {
document.body.classList.remove(p_class);
}
}
registerWorker(p_worker) {
this.workers.set(p_worker.name, p_worker);
@ -79,6 +107,7 @@ class VNoteX extends EventEmitter {
if (this.numOfOngoingWorkers == 0) {
// Signal out anyway.
this.emit('fullMarkdownRendered');
window.vxMarkdownAdapter.setWorkFinished();
// Check pending work.
if (this.pendingData.text) {
@ -211,13 +240,8 @@ class VNoteX extends EventEmitter {
setSectionNumberEnabled(p_enabled) {
let sectionClass = 'vx-section-number';
let sectionLevelClass = 'vx-section-number-' + this.sectionNumberBaseLevel;
if (p_enabled) {
this.contentContainer.classList.add(sectionClass);
this.contentContainer.classList.add(sectionLevelClass);
} else {
this.contentContainer.classList.remove(sectionClass);
this.contentContainer.classList.remove(sectionLevelClass);
}
this.setContentContainerOption(sectionClass, p_enabled);
this.setContentContainerOption(sectionLevelClass, p_enabled);
}
scroll(p_up) {
@ -261,6 +285,28 @@ class VNoteX extends EventEmitter {
window.vxMarkdownAdapter.setFindText(p_text, p_totalMatches, p_currentMatchIndex);
}
saveContent() {
if (!this.initialized) {
console.warn('saveContent() called before initialization');
window.vxMarkdownAdapter.setSavedContent('', '', '');
return;
}
window.vxMarkdownAdapter.setSavedContent("",
Utils.fetchStyleContent(),
this.contentContainer.outerHTML,
document.body.classList.value);
}
setBodySize(p_width, p_height) {
if (p_width > 0) {
document.body.style.width = p_width + 'px';
}
if (p_height > 0) {
document.body.style.height = p_height + 'px';
}
}
static detectOS() {
let osName="Unknown OS";
if (navigator.appVersion.indexOf("Win")!=-1) {

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="generator" content="VNote">
<!-- VX_TITLE_PLACEHOLDER -->
<style type="text/css">
/* VX_GLOBAL_STYLES_PLACEHOLDER */
</style>
<style type="text/css">
/* VX_STYLES_PLACEHOLDER */
/* VX_STYLES_CONTENT_PLACEHOLDER */
</style>
<script type="text/javascript">
/* VX_SCRIPTS_PLACEHOLDER */
</script>
<!-- VX_SCRIPTS_PLACEHOLDER -->
<!-- VX_HEAD_PLACEHOLDER -->
</head>
<body class="<!-- VX_BODY_CLASS_LIST_PLACEHOLDER -->">
<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">
<!-- VX_CONTENT_PLACEHOLDER -->
</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>

11
src/export/export.pri Normal file
View File

@ -0,0 +1,11 @@
QT += widgets
SOURCES += \
$$PWD/exportdata.cpp \
$$PWD/exporter.cpp \
$$PWD/webviewexporter.cpp
HEADERS += \
$$PWD/exportdata.h \
$$PWD/exporter.h \
$$PWD/webviewexporter.h

126
src/export/exportdata.cpp Normal file
View File

@ -0,0 +1,126 @@
#include "exportdata.h"
#include <QPageLayout>
#include <QJsonObject>
#include <QJsonArray>
using namespace vnotex;
QJsonObject ExportHtmlOption::toJson() const
{
QJsonObject obj;
obj["embed_styles"] = m_embedStyles;
obj["complete_page"] = m_completePage;
obj["embed_images"] = m_embedImages;
obj["use_mime_html_format"] = m_useMimeHtmlFormat;
obj["add_outline_panel"] = m_addOutlinePanel;
return obj;
}
void ExportHtmlOption::fromJson(const QJsonObject &p_obj)
{
if (p_obj.isEmpty()) {
return;
}
m_embedStyles = p_obj["embed_styles"].toBool();
m_completePage = p_obj["complete_page"].toBool();
m_embedImages = p_obj["embed_images"].toBool();
m_useMimeHtmlFormat = p_obj["use_mime_html_format"].toBool();
m_addOutlinePanel = p_obj["add_outline_panel"].toBool();
}
bool ExportHtmlOption::operator==(const ExportHtmlOption &p_other) const
{
return m_embedStyles == p_other.m_embedStyles
&& m_completePage == p_other.m_completePage
&& m_embedImages == p_other.m_embedImages
&& m_useMimeHtmlFormat == p_other.m_useMimeHtmlFormat
&& m_addOutlinePanel == p_other.m_addOutlinePanel;
}
ExportPdfOption::ExportPdfOption()
: m_layout(new QPageLayout(QPageSize(QPageSize::A4),
QPageLayout::Portrait,
QMarginsF(10, 16, 10, 10),
QPageLayout::Millimeter))
{
}
QJsonObject ExportPdfOption::toJson() const
{
QJsonObject obj;
obj["add_table_of_contents"] = m_addTableOfContents;
obj["use_wkhtmltopdf"] = m_useWkhtmltopdf;
obj["wkhtmltopdf_exe_path"] = m_wkhtmltopdfExePath;
obj["wkhtmltopdf_args"] = m_wkhtmltopdfArgs;
return obj;
}
void ExportPdfOption::fromJson(const QJsonObject &p_obj)
{
if (p_obj.isEmpty()) {
return;
}
m_addTableOfContents = p_obj["add_table_of_contents"].toBool();
m_useWkhtmltopdf = p_obj["use_wkhtmltopdf"].toBool();
m_wkhtmltopdfExePath = p_obj["wkhtmltopdf_exe_path"].toString();
m_wkhtmltopdfArgs = p_obj["wkhtmltopdf_args"].toString();
}
bool ExportPdfOption::operator==(const ExportPdfOption &p_other) const
{
return m_addTableOfContents == p_other.m_addTableOfContents
&& m_useWkhtmltopdf == p_other.m_useWkhtmltopdf
&& m_wkhtmltopdfExePath == p_other.m_wkhtmltopdfExePath
&& m_wkhtmltopdfArgs == p_other.m_wkhtmltopdfArgs;
}
QJsonObject ExportOption::toJson() const
{
QJsonObject obj;
obj["use_transparent_bg"] = m_useTransparentBg;
obj["output_dir"] = m_outputDir;
obj["recursive"] = m_recursive;
obj["export_attachments"] = m_exportAttachments;
obj["html_option"] = m_htmlOption.toJson();
obj["pdf_option"] = m_pdfOption.toJson();
return obj;
}
void ExportOption::fromJson(const QJsonObject &p_obj)
{
if (p_obj.isEmpty()) {
return;
}
m_useTransparentBg = p_obj["use_transparent_bg"].toBool();
m_outputDir = p_obj["output_dir"].toString();
m_recursive = p_obj["recursive"].toBool();
m_exportAttachments = p_obj["export_attachments"].toBool();
m_htmlOption.fromJson(p_obj["html_option"].toObject());
m_pdfOption.fromJson(p_obj["pdf_option"].toObject());
}
bool ExportOption::operator==(const ExportOption &p_other) const
{
bool ret = m_useTransparentBg == p_other.m_useTransparentBg
&& m_outputDir == p_other.m_outputDir
&& m_recursive == p_other.m_recursive
&& m_exportAttachments == p_other.m_exportAttachments;
if (!ret) {
return false;
}
if (!(m_htmlOption == p_other.m_htmlOption)) {
return false;
}
if (!(m_pdfOption == p_other.m_pdfOption)) {
return false;
}
return true;
}

114
src/export/exportdata.h Normal file
View File

@ -0,0 +1,114 @@
#ifndef EXPORTDATA_H
#define EXPORTDATA_H
#include <QSharedPointer>
class QPageLayout;
namespace vnotex
{
enum class ExportSource
{
CurrentBuffer = 0,
CurrentFolder,
CurrentNotebook
};
enum class ExportFormat
{
Markdown = 0,
HTML,
PDF,
Custom
};
struct ExportHtmlOption
{
QJsonObject toJson() const;
void fromJson(const QJsonObject &p_obj);
bool operator==(const ExportHtmlOption &p_other) const;
bool m_embedStyles = true;
bool m_completePage = true;
bool m_embedImages = true;
bool m_useMimeHtmlFormat = false;
// Whether add outline panel.
bool m_addOutlinePanel = true;
};
struct ExportPdfOption
{
ExportPdfOption();
QJsonObject toJson() const;
void fromJson(const QJsonObject &p_obj);
bool operator==(const ExportPdfOption &p_other) const;
QSharedPointer<QPageLayout> m_layout;
// Add TOC at the front to complement the missing of outline.
bool m_addTableOfContents = false;
bool m_useWkhtmltopdf = false;
QString m_wkhtmltopdfExePath;
QString m_wkhtmltopdfArgs;
};
struct ExportOption
{
QJsonObject toJson() const;
void fromJson(const QJsonObject &p_obj);
bool operator==(const ExportOption &p_other) const;
ExportSource m_source = ExportSource::CurrentBuffer;
ExportFormat m_targetFormat = ExportFormat::HTML;
bool m_useTransparentBg = true;
QString m_renderingStyleFile;
QString m_syntaxHighlightStyleFile;
QString m_outputDir;
bool m_recursive = true;
bool m_exportAttachments = true;
ExportHtmlOption m_htmlOption;
ExportPdfOption m_pdfOption;
};
inline QString exportFormatString(ExportFormat p_format)
{
switch (p_format)
{
case ExportFormat::Markdown:
return QStringLiteral("Markdown");
case ExportFormat::HTML:
return QStringLiteral("HTML");
case ExportFormat::PDF:
return QStringLiteral("PDF");
case ExportFormat::Custom:
return QStringLiteral("Custom");
}
return QStringLiteral("Unknown");
}
}
#endif // EXPORTDATA_H

312
src/export/exporter.cpp Normal file
View File

@ -0,0 +1,312 @@
#include "exporter.h"
#include <QWidget>
#include <notebook/notebook.h>
#include <notebook/node.h>
#include <buffer/buffer.h>
#include <core/file.h>
#include <utils/fileutils.h>
#include <utils/pathutils.h>
#include <utils/contentmediautils.h>
#include "webviewexporter.h"
using namespace vnotex;
Exporter::Exporter(QWidget *p_parent)
: QObject(p_parent)
{
}
QString Exporter::doExport(const ExportOption &p_option, Buffer *p_buffer)
{
m_askedToStop = false;
QString outputFile;
auto file = p_buffer->getFile();
if (!file) {
emit logRequested(tr("Skipped buffer (%1) without file base.").arg(p_buffer->getName()));
return outputFile;
}
// Make sure output folder exists.
if (!QDir().mkpath(p_option.m_outputDir)) {
emit logRequested(tr("Failed to create output folder %1.").arg(p_option.m_outputDir));
return outputFile;
}
outputFile = doExport(p_option, p_option.m_outputDir, file.data());
cleanUp();
return outputFile;
}
QString Exporter::doExportMarkdown(const ExportOption &p_option, const QString &p_outputDir, const File *p_file)
{
QString outputFile;
if (!p_file->getContentType().isMarkdown()) {
emit logRequested(tr("Format %1 is not supported to export as Markdown.").arg(p_file->getContentType().m_displayName));
return outputFile;
}
// Export it to a folder with the same name.
auto name = FileUtils::generateFileNameWithSequence(p_outputDir, p_file->getName(), "");
auto outputFolder = PathUtils::concatenateFilePath(p_outputDir, name);
QDir outDir(outputFolder);
if (!outDir.mkpath(outputFolder)) {
emit logRequested(tr("Failed to create output folder %1.").arg(outputFolder));
return outputFile;
}
// Copy source file itself.
const auto srcFilePath = p_file->getFilePath();
auto destFilePath = outDir.filePath(p_file->getName());
FileUtils::copyFile(srcFilePath, destFilePath, false);
outputFile = destFilePath;
ContentMediaUtils::copyMediaFiles(p_file, destFilePath);
// Copy attachments if available.
if (p_option.m_exportAttachments) {
exportAttachments(p_file->getNode(), srcFilePath, outputFolder, destFilePath);
}
return outputFile;
}
void Exporter::exportAttachments(Node *p_node,
const QString &p_srcFilePath,
const QString &p_outputFolder,
const QString &p_destFilePath)
{
const auto &attachmentFolder = p_node->getAttachmentFolder();
if (!attachmentFolder.isEmpty()) {
auto relativePath = PathUtils::relativePath(PathUtils::parentDirPath(p_srcFilePath),
p_node->fetchAttachmentFolderPath());
auto destAttachmentFolderPath = QDir(p_outputFolder).filePath(relativePath);
destAttachmentFolderPath = FileUtils::renameIfExistsCaseInsensitive(destAttachmentFolderPath);
ContentMediaUtils::copyAttachment(p_node, nullptr, p_destFilePath, destAttachmentFolderPath);
}
}
QStringList Exporter::doExport(const ExportOption &p_option, Node *p_folder)
{
m_askedToStop = false;
auto outputFiles = doExport(p_option, p_option.m_outputDir, p_folder);
cleanUp();
return outputFiles;
}
QStringList Exporter::doExport(const ExportOption &p_option, const QString &p_outputDir, Node *p_folder)
{
Q_ASSERT(p_folder->isContainer());
QStringList outputFiles;
// Make path.
auto name = FileUtils::generateFileNameWithSequence(p_outputDir, p_folder->getName());
auto outputFolder = PathUtils::concatenateFilePath(p_outputDir, name);
if (!QDir().mkpath(outputFolder)) {
emit logRequested(tr("Failed to create output folder %1.").arg(outputFolder));
return outputFiles;
}
p_folder->load();
const auto &children = p_folder->getChildren();
emit progressUpdated(0, children.size());
for (int i = 0; i < children.size(); ++i) {
if (checkAskedToStop()) {
break;
}
const auto &child = children[i];
if (child->hasContent()) {
auto outputFile = doExport(p_option, outputFolder, child->getContentFile().data());
if (!outputFile.isEmpty()) {
outputFiles << outputFile;
}
}
if (p_option.m_recursive && child->isContainer() && child->getUse() == Node::Use::Normal) {
outputFiles.append(doExport(p_option, outputFolder, child.data()));
}
emit progressUpdated(i + 1, children.size());
}
return outputFiles;
}
QString Exporter::doExport(const ExportOption &p_option, const QString &p_outputDir, const File *p_file)
{
QString outputFile;
switch (p_option.m_targetFormat) {
case ExportFormat::Markdown:
outputFile = doExportMarkdown(p_option, p_outputDir, p_file);
break;
case ExportFormat::HTML:
outputFile = doExportHtml(p_option, p_outputDir, p_file);
break;
case ExportFormat::PDF:
outputFile = doExportPdf(p_option, p_outputDir, p_file);
break;
default:
emit logRequested(tr("Unknown target format %1.").arg(exportFormatString(p_option.m_targetFormat)));
break;
}
if (!outputFile.isEmpty()) {
emit logRequested(tr("File (%1) exported to (%2)").arg(p_file->getFilePath(), outputFile));
} else {
emit logRequested(tr("Failed to export file (%1)").arg(p_file->getFilePath()));
}
return outputFile;
}
QStringList Exporter::doExport(const ExportOption &p_option, Notebook *p_notebook)
{
m_askedToStop = false;
QStringList outputFiles;
// Make path.
auto name = FileUtils::generateFileNameWithSequence(p_option.m_outputDir,
tr("notebook_%1").arg(p_notebook->getName()));
auto outputFolder = PathUtils::concatenateFilePath(p_option.m_outputDir, name);
if (!QDir().mkpath(outputFolder)) {
emit logRequested(tr("Failed to create output folder %1.").arg(outputFolder));
return outputFiles;
}
auto rootNode = p_notebook->getRootNode();
Q_ASSERT(rootNode->isLoaded());
const auto &children = rootNode->getChildren();
emit progressUpdated(0, children.size());
for (int i = 0; i < children.size(); ++i) {
if (checkAskedToStop()) {
break;
}
const auto &child = children[i];
if (child->hasContent()) {
auto outputFile = doExport(p_option, outputFolder, child->getContentFile().data());
if (!outputFile.isEmpty()) {
outputFiles << outputFile;
}
}
if (child->isContainer() && child->getUse() == Node::Use::Normal) {
outputFiles.append(doExport(p_option, outputFolder, child.data()));
}
emit progressUpdated(i + 1, children.size());
}
cleanUp();
return outputFiles;
}
QString Exporter::doExportHtml(const ExportOption &p_option, const QString &p_outputDir, const File *p_file)
{
QString outputFile;
if (!p_file->getContentType().isMarkdown()) {
emit logRequested(tr("Format %1 is not supported to export as HTML.").arg(p_file->getContentType().m_displayName));
return outputFile;
}
QString suffix = p_option.m_htmlOption.m_useMimeHtmlFormat ? QStringLiteral("mht") : QStringLiteral("html");
auto fileName = FileUtils::generateFileNameWithSequence(p_outputDir,
QFileInfo(p_file->getName()).completeBaseName(),
suffix);
auto destFilePath = PathUtils::concatenateFilePath(p_outputDir, fileName);
bool success = getWebViewExporter(p_option)->doExport(p_option, p_file, destFilePath);
if (success) {
outputFile = destFilePath;
// Copy attachments if available.
if (p_option.m_exportAttachments) {
exportAttachments(p_file->getNode(), p_file->getFilePath(), p_outputDir, destFilePath);
}
}
return outputFile;
}
WebViewExporter *Exporter::getWebViewExporter(const ExportOption &p_option)
{
if (!m_webViewExporter) {
m_webViewExporter = new WebViewExporter(static_cast<QWidget *>(parent()));
connect(m_webViewExporter, &WebViewExporter::logRequested,
this, &Exporter::logRequested);
m_webViewExporter->prepare(p_option);
}
return m_webViewExporter;
}
void Exporter::cleanUpWebViewExporter()
{
if (m_webViewExporter) {
m_webViewExporter->clear();
delete m_webViewExporter;
m_webViewExporter = nullptr;
}
}
void Exporter::cleanUp()
{
cleanUpWebViewExporter();
}
void Exporter::stop()
{
m_askedToStop = true;
if (m_webViewExporter) {
m_webViewExporter->stop();
}
}
bool Exporter::checkAskedToStop() const
{
if (m_askedToStop) {
emit const_cast<Exporter *>(this)->logRequested(tr("Asked to stop. Aborting."));
return true;
}
return false;
}
QString Exporter::doExportPdf(const ExportOption &p_option, const QString &p_outputDir, const File *p_file)
{
QString outputFile;
if (!p_file->getContentType().isMarkdown()) {
emit logRequested(tr("Format %1 is not supported to export as PDF.").arg(p_file->getContentType().m_displayName));
return outputFile;
}
auto fileName = FileUtils::generateFileNameWithSequence(p_outputDir,
QFileInfo(p_file->getName()).completeBaseName(),
"pdf");
auto destFilePath = PathUtils::concatenateFilePath(p_outputDir, fileName);
bool success = getWebViewExporter(p_option)->doExport(p_option, p_file, destFilePath);
if (success) {
outputFile = destFilePath;
// Copy attachments if available.
if (p_option.m_exportAttachments) {
exportAttachments(p_file->getNode(), p_file->getFilePath(), p_outputDir, destFilePath);
}
}
return outputFile;
}

70
src/export/exporter.h Normal file
View File

@ -0,0 +1,70 @@
#ifndef EXPORTER_H
#define EXPORTER_H
#include <QObject>
#include <QStringList>
#include "exportdata.h"
namespace vnotex
{
class Notebook;
class Node;
class Buffer;
class File;
class WebViewExporter;
class Exporter : public QObject
{
Q_OBJECT
public:
// We need the QWidget as parent.
explicit Exporter(QWidget *p_parent);
// Return exported output file.
QString doExport(const ExportOption &p_option, Buffer *p_buffer);
// Return exported output files.
QStringList doExport(const ExportOption &p_option, Node *p_folder);
QStringList doExport(const ExportOption &p_option, Notebook *p_notebook);
void stop();
signals:
void progressUpdated(int p_val, int p_maximum);
void logRequested(const QString &p_log);
private:
QStringList doExport(const ExportOption &p_option, const QString &p_outputDir, Node *p_folder);
QString doExport(const ExportOption &p_option, const QString &p_outputDir, const File *p_file);
QString doExportMarkdown(const ExportOption &p_option, const QString &p_outputDir, const File *p_file);
QString doExportHtml(const ExportOption &p_option, const QString &p_outputDir, const File *p_file);
QString doExportPdf(const ExportOption &p_option, const QString &p_outputDir, const File *p_file);
void exportAttachments(Node *p_node,
const QString &p_srcFilePath,
const QString &p_outputFolder,
const QString &p_destFilePath);
WebViewExporter *getWebViewExporter(const ExportOption &p_option);
void cleanUpWebViewExporter();
void cleanUp();
bool checkAskedToStop() const;
// Managed by QObject.
WebViewExporter *m_webViewExporter = nullptr;
bool m_askedToStop = false;
};
}
#endif // EXPORTER_H

View File

@ -0,0 +1,568 @@
#include "webviewexporter.h"
#include <QWidget>
#include <QWebEnginePage>
#include <QFileInfo>
#include <QTemporaryDir>
#include <QProcess>
#include <widgets/editors/markdownviewer.h>
#include <widgets/editors/editormarkdownvieweradapter.h>
#include <core/editorconfig.h>
#include <core/markdowneditorconfig.h>
#include <core/configmgr.h>
#include <core/htmltemplatehelper.h>
#include <utils/utils.h>
#include <utils/pathutils.h>
#include <utils/fileutils.h>
#include <utils/webutils.h>
#include <utils/processutils.h>
#include <core/file.h>
using namespace vnotex;
static const QString c_imgRegExp = "<img ([^>]*)src=\"(?!data:)([^\"]+)\"([^>]*)>";
WebViewExporter::WebViewExporter(QWidget *p_parent)
: QObject(p_parent)
{
}
WebViewExporter::~WebViewExporter()
{
clear();
}
void WebViewExporter::clear()
{
m_askedToStop = false;
delete m_viewer;
m_viewer = nullptr;
m_htmlTemplate.clear();
m_exportHtmlTemplate.clear();
m_exportOngoing = false;
}
bool WebViewExporter::doExport(const ExportOption &p_option,
const File *p_file,
const QString &p_outputFile)
{
bool ret = false;
m_askedToStop = false;
Q_ASSERT(p_file->getContentType().isMarkdown());
Q_ASSERT(!m_exportOngoing);
m_exportOngoing = true;
m_webViewStates = WebViewState::Started;
auto baseUrl = PathUtils::pathToUrl(p_file->getContentPath());
m_viewer->setHtml(m_htmlTemplate, baseUrl);
auto textContent = p_file->read();
if (p_option.m_targetFormat == ExportFormat::PDF
&& p_option.m_pdfOption.m_addTableOfContents
&& !p_option.m_pdfOption.m_useWkhtmltopdf) {
// Add `[TOC]` at the beginning.
m_viewer->adapter()->setText("[TOC]\n\n" + textContent);
} else {
m_viewer->adapter()->setText(textContent);
}
while (!isWebViewReady()) {
Utils::sleepWait(100);
if (m_askedToStop) {
goto exit_export;
}
if (isWebViewFailed()) {
qWarning() << "WebView failed when exporting" << p_file->getFilePath();
goto exit_export;
}
}
qDebug() << "WebView is ready";
// Add extra wait to make sure Web side is really ready.
Utils::sleepWait(200);
switch (p_option.m_targetFormat) {
case ExportFormat::HTML:
// TODO: not supported yet.
Q_ASSERT(!p_option.m_htmlOption.m_useMimeHtmlFormat);
ret = doExportHtml(p_option.m_htmlOption, p_outputFile, baseUrl);
break;
case ExportFormat::PDF:
if (p_option.m_pdfOption.m_useWkhtmltopdf) {
ret = doExportWkhtmltopdf(p_option.m_pdfOption, p_outputFile, baseUrl);
} else {
ret = doExportPdf(p_option.m_pdfOption, p_outputFile);
}
break;
default:
break;
}
exit_export:
m_exportOngoing = false;
return ret;
}
void WebViewExporter::stop()
{
m_askedToStop = true;
}
bool WebViewExporter::isWebViewReady() const
{
return m_webViewStates == (WebViewState::LoadFinished | WebViewState::WorkFinished);
}
bool WebViewExporter::isWebViewFailed() const
{
return m_webViewStates & WebViewState::Failed;
}
bool WebViewExporter::doExportHtml(const ExportHtmlOption &p_htmlOption,
const QString &p_outputFile,
const QUrl &p_baseUrl)
{
ExportState state = ExportState::Busy;
connect(m_viewer->adapter(), &MarkdownViewerAdapter::contentReady,
this, [&, this](const QString &p_headContent,
const QString &p_styleContent,
const QString &p_content,
const QString &p_bodyClassList) {
qDebug() << "doExportHtml contentReady";
// Maybe unnecessary. Just to avoid duplicated signal connections.
disconnect(m_viewer->adapter(), &MarkdownViewerAdapter::contentReady, this, 0);
if (p_content.isEmpty() || m_askedToStop) {
state = ExportState::Failed;
return;
}
if (!writeHtmlFile(p_outputFile,
p_baseUrl,
p_headContent,
p_styleContent,
p_content,
p_bodyClassList,
p_htmlOption.m_embedStyles,
p_htmlOption.m_completePage,
p_htmlOption.m_embedImages)) {
state = ExportState::Failed;
return;
}
state = ExportState::Finished;
});
m_viewer->adapter()->saveContent();
while (state == ExportState::Busy) {
Utils::sleepWait(100);
if (m_askedToStop) {
break;
}
}
return state == ExportState::Finished;
}
bool WebViewExporter::writeHtmlFile(const QString &p_file,
const QUrl &p_baseUrl,
const QString &p_headContent,
QString p_styleContent,
const QString &p_content,
const QString &p_bodyClassList,
bool p_embedStyles,
bool p_completePage,
bool p_embedImages)
{
const auto baseName = QFileInfo(p_file).completeBaseName();
auto title = QString("%1 - %2").arg(baseName, ConfigMgr::c_appName);
const QString resourceFolderName = baseName + "_files";
auto resourceFolder = PathUtils::concatenateFilePath(PathUtils::parentDirPath(p_file), resourceFolderName);
qDebug() << "HTML files folder" << resourceFolder;
auto htmlContent = m_exportHtmlTemplate;
HtmlTemplateHelper::fillTitle(htmlContent, title);
if (!p_styleContent.isEmpty() && p_embedStyles) {
embedStyleResources(p_styleContent);
HtmlTemplateHelper::fillStyleContent(htmlContent, p_styleContent);
}
if (!p_headContent.isEmpty()) {
HtmlTemplateHelper::fillHeadContent(htmlContent, p_headContent);
}
if (p_completePage) {
QString content(p_content);
if (p_embedImages) {
embedBodyResources(p_baseUrl, content);
} else {
fixBodyResources(p_baseUrl, resourceFolder, content);
}
HtmlTemplateHelper::fillContent(htmlContent, content);
} else {
HtmlTemplateHelper::fillContent(htmlContent, p_content);
}
if (!p_bodyClassList.isEmpty()) {
HtmlTemplateHelper::fillBodyClassList(htmlContent, p_bodyClassList);
}
FileUtils::writeFile(p_file, htmlContent);
// Delete empty resource folder.
QDir dir(resourceFolder);
if (dir.exists() && dir.isEmpty()) {
dir.cdUp();
dir.rmdir(resourceFolderName);
}
return true;
}
QSize WebViewExporter::pageLayoutSize(const QPageLayout &p_layout) const
{
Q_ASSERT(m_viewer);
auto rect = p_layout.paintRect(QPageLayout::Inch);
return QSize(rect.width() * m_viewer->logicalDpiX(), rect.height() * m_viewer->logicalDpiY());
}
void WebViewExporter::prepare(const ExportOption &p_option)
{
Q_ASSERT(!m_viewer && !m_exportOngoing);
{
// Adapter will be managed by MarkdownViewer.
auto adapter = new MarkdownViewerAdapter(this);
m_viewer = new MarkdownViewer(adapter, QColor(), 1, static_cast<QWidget *>(parent()));
m_viewer->hide();
connect(m_viewer->page(), &QWebEnginePage::loadFinished,
this, [this]() {
m_webViewStates |= WebViewState::LoadFinished;
});
connect(adapter, &MarkdownViewerAdapter::workFinished,
this, [this]() {
m_webViewStates |= WebViewState::WorkFinished;
});
}
const bool scrollable = p_option.m_targetFormat != ExportFormat::PDF;
const auto &config = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig();
bool useWkhtmltopdf = false;
QSize pageBodySize(1024, 768);
if (p_option.m_targetFormat == ExportFormat::PDF) {
useWkhtmltopdf = p_option.m_pdfOption.m_useWkhtmltopdf;
pageBodySize = pageLayoutSize(*(p_option.m_pdfOption.m_layout));
}
qDebug() << "export page body size" << pageBodySize;
m_htmlTemplate = HtmlTemplateHelper::generateMarkdownViewerTemplate(config,
p_option.m_renderingStyleFile,
p_option.m_syntaxHighlightStyleFile,
p_option.m_useTransparentBg,
scrollable,
pageBodySize.width(),
pageBodySize.height(),
useWkhtmltopdf,
useWkhtmltopdf ? 2.5 : -1);
{
const bool addOutlinePanel = p_option.m_targetFormat == ExportFormat::HTML && p_option.m_htmlOption.m_addOutlinePanel;
m_exportHtmlTemplate = HtmlTemplateHelper::generateExportTemplate(config, addOutlinePanel);
}
if (useWkhtmltopdf) {
prepareWkhtmltopdfArguments(p_option.m_pdfOption);
}
}
static QString marginToStrMM(qreal p_margin)
{
return QString("%1mm").arg(p_margin);
}
void WebViewExporter::prepareWkhtmltopdfArguments(const ExportPdfOption &p_pdfOption)
{
m_wkhtmltopdfArgs.clear();
// Page layout.
{
const auto &layout = p_pdfOption.m_layout;
m_wkhtmltopdfArgs << "--page-size" << layout->pageSize().key();
m_wkhtmltopdfArgs << "--orientation"
<< (layout->orientation() == QPageLayout::Portrait ? "Portrait" : "Landscape");
const auto marginsMM = layout->margins(QPageLayout::Millimeter);
m_wkhtmltopdfArgs << "--margin-bottom" << marginToStrMM(marginsMM.bottom());
m_wkhtmltopdfArgs << "--margin-left" << marginToStrMM(marginsMM.left());
m_wkhtmltopdfArgs << "--margin-right" << marginToStrMM(marginsMM.right());
m_wkhtmltopdfArgs << "--margin-top" << marginToStrMM(marginsMM.top());
// Footer.
m_wkhtmltopdfArgs << "--footer-right" << "[page]"
<< "--footer-spacing" << QString::number(marginsMM.bottom() / 3, 'f', 2);
}
m_wkhtmltopdfArgs << "--encoding" << "utf-8";
// Delay 10 seconds for MathJax.
m_wkhtmltopdfArgs << "--javascript-delay" << "5000";
m_wkhtmltopdfArgs << "--enable-local-file-access";
// Append additional global option.
if (!p_pdfOption.m_wkhtmltopdfArgs.isEmpty()) {
m_wkhtmltopdfArgs.append(ProcessUtils::parseCombinedArgString(p_pdfOption.m_wkhtmltopdfArgs));
}
// Must be put after the global object options.
if (p_pdfOption.m_addTableOfContents) {
m_wkhtmltopdfArgs << "toc" << "--toc-text-size-shrink" << "1.0";
}
}
bool WebViewExporter::embedStyleResources(QString &p_html) const
{
bool altered = false;
QRegExp reg("\\burl\\(\"((file|qrc):[^\"\\)]+)\"\\);");
int pos = 0;
while (pos < p_html.size()) {
int idx = p_html.indexOf(reg, pos);
if (idx == -1) {
break;
}
QString dataURI = WebUtils::toDataUri(QUrl(reg.cap(1)), false);
if (dataURI.isEmpty()) {
pos = idx + reg.matchedLength();
} else {
// Replace the url string in html.
QString newUrl = QString("url('%1');").arg(dataURI);
p_html.replace(idx, reg.matchedLength(), newUrl);
pos = idx + newUrl.size();
altered = true;
}
}
return altered;
}
bool WebViewExporter::embedBodyResources(const QUrl &p_baseUrl, QString &p_html)
{
bool altered = false;
if (p_baseUrl.isEmpty()) {
return altered;
}
QRegExp reg(c_imgRegExp);
int pos = 0;
while (pos < p_html.size()) {
int idx = p_html.indexOf(reg, pos);
if (idx == -1) {
break;
}
if (reg.cap(2).isEmpty()) {
pos = idx + reg.matchedLength();
continue;
}
QUrl srcUrl(p_baseUrl.resolved(reg.cap(2)));
const auto dataURI = WebUtils::toDataUri(srcUrl, true);
if (dataURI.isEmpty()) {
pos = idx + reg.matchedLength();
} else {
// Replace the url string in html.
QString newUrl = QString("<img %1src='%2'%3>").arg(reg.cap(1), dataURI, reg.cap(3));
p_html.replace(idx, reg.matchedLength(), newUrl);
pos = idx + newUrl.size();
altered = true;
}
}
return altered;
}
static QString getResourceRelativePath(const QString &p_file)
{
int idx = p_file.lastIndexOf('/');
int idx2 = p_file.lastIndexOf('/', idx - 1);
Q_ASSERT(idx > 0 && idx2 < idx);
return "." + p_file.mid(idx2);
}
bool WebViewExporter::fixBodyResources(const QUrl &p_baseUrl,
const QString &p_folder,
QString &p_html)
{
bool altered = false;
if (p_baseUrl.isEmpty()) {
return altered;
}
QRegExp reg(c_imgRegExp);
int pos = 0;
while (pos < p_html.size()) {
int idx = p_html.indexOf(reg, pos);
if (idx == -1) {
break;
}
if (reg.cap(2).isEmpty()) {
pos = idx + reg.matchedLength();
continue;
}
QUrl srcUrl(p_baseUrl.resolved(reg.cap(2)));
QString targetFile = WebUtils::copyResource(srcUrl, p_folder);
if (targetFile.isEmpty()) {
pos = idx + reg.matchedLength();
} else {
// Replace the url string in html.
QString newUrl = QString("<img %1src=\"%2\"%3>").arg(reg.cap(1), getResourceRelativePath(targetFile), reg.cap(3));
p_html.replace(idx, reg.matchedLength(), newUrl);
pos = idx + newUrl.size();
altered = true;
}
}
return altered;
}
bool WebViewExporter::doExportPdf(const ExportPdfOption &p_pdfOption, const QString &p_outputFile)
{
ExportState state = ExportState::Busy;
m_viewer->page()->printToPdf([&, this](const QByteArray &p_result) {
qDebug() << "doExportPdf printToPdf ready";
if (p_result.isEmpty() || m_askedToStop) {
state = ExportState::Failed;
return;
}
Q_ASSERT(!p_outputFile.isEmpty());
FileUtils::writeFile(p_outputFile, p_result);
state = ExportState::Finished;
}, *p_pdfOption.m_layout);
while (state == ExportState::Busy) {
Utils::sleepWait(100);
if (m_askedToStop) {
break;
}
}
return state == ExportState::Finished;
}
bool WebViewExporter::doExportWkhtmltopdf(const ExportPdfOption &p_pdfOption, const QString &p_outputFile, const QUrl &p_baseUrl)
{
ExportState state = ExportState::Busy;
connect(m_viewer->adapter(), &MarkdownViewerAdapter::contentReady,
this, [&, this](const QString &p_headContent,
const QString &p_styleContent,
const QString &p_content,
const QString &p_bodyClassList) {
qDebug() << "doExportWkhtmltopdf contentReady";
// Maybe unnecessary. Just to avoid duplicated signal connections.
disconnect(m_viewer->adapter(), &MarkdownViewerAdapter::contentReady, this, 0);
if (p_content.isEmpty() || m_askedToStop) {
state = ExportState::Failed;
return;
}
// Save HTML to a temp dir.
QTemporaryDir tmpDir;
if (!tmpDir.isValid()) {
state = ExportState::Failed;
return;
}
auto tmpHtmlFile = tmpDir.filePath("vnote_export_tmp.html");
if (!writeHtmlFile(tmpHtmlFile,
p_baseUrl,
p_headContent,
p_styleContent,
p_content,
p_bodyClassList,
true,
true,
false)) {
state = ExportState::Failed;
return;
}
// Convert HTML to PDF via wkhtmltopdf.
if (doWkhtmltopdf(p_pdfOption, QStringList() << tmpHtmlFile, p_outputFile)) {
state = ExportState::Finished;
} else {
state = ExportState::Failed;
}
});
m_viewer->adapter()->saveContent();
while (state == ExportState::Busy) {
Utils::sleepWait(100);
if (m_askedToStop) {
break;
}
}
return state == ExportState::Finished;
}
bool WebViewExporter::doWkhtmltopdf(const ExportPdfOption &p_pdfOption, const QStringList &p_htmlFiles, const QString &p_outputFile)
{
// Note: system's locale settings (Language for non-Unicode programs) is important to wkhtmltopdf.
// Input file could be encoded via QUrl::fromLocalFile(p_htmlFile).toString(QUrl::EncodeUnicode) to
// handle non-ASCII path.
QStringList args(m_wkhtmltopdfArgs);
// Prepare the args.
for (auto const &file : p_htmlFiles) {
args << QDir::toNativeSeparators(file);
}
args << QDir::toNativeSeparators(p_outputFile);
return startProcess(p_pdfOption.m_wkhtmltopdfExePath, args);
}
bool WebViewExporter::startProcess(const QString &p_program, const QStringList &p_args)
{
emit logRequested(p_program + " " + ProcessUtils::combineArgString(p_args));
auto ret = ProcessUtils::start(p_program,
p_args,
[this](const QString &p_log) {
emit logRequested(p_log);
},
m_askedToStop);
return ret == ProcessUtils::State::Succeeded;
}

View File

@ -0,0 +1,110 @@
#ifndef WEBVIEWEXPORTER_H
#define WEBVIEWEXPORTER_H
#include <QObject>
#include "exportdata.h"
class QWidget;
namespace vnotex
{
class File;
class MarkdownViewer;
class WebViewExporter : public QObject
{
Q_OBJECT
public:
enum WebViewState
{
Started = 0,
LoadFinished = 0x1,
WorkFinished = 0x2,
Failed = 0x4
};
Q_DECLARE_FLAGS(WebViewStates, WebViewState);
// We need QWidget as parent.
explicit WebViewExporter(QWidget *p_parent);
~WebViewExporter();
bool doExport(const ExportOption &p_option,
const File *p_file,
const QString &p_outputFile);
void prepare(const ExportOption &p_option);
// Release resources after one batch of export.
void clear();
void stop();
signals:
void logRequested(const QString &p_log);
private:
enum class ExportState
{
Busy = 0,
Finished,
Failed
};
bool isWebViewReady() const;
bool isWebViewFailed() const;
bool doExportHtml(const ExportHtmlOption &p_htmlOption,
const QString &p_outputFile,
const QUrl &p_baseUrl);
bool writeHtmlFile(const QString &p_file,
const QUrl &p_baseUrl,
const QString &p_headContent,
QString p_styleContent,
const QString &p_content,
const QString &p_bodyClassList,
bool p_embedStyles,
bool p_completePage,
bool p_embedImages);
bool embedStyleResources(QString &p_html) const;
bool embedBodyResources(const QUrl &p_baseUrl, QString &p_html);
bool fixBodyResources(const QUrl &p_baseUrl, const QString &p_folder, QString &p_html);
bool doExportPdf(const ExportPdfOption &p_pdfOption, const QString &p_outputFile);
bool doExportWkhtmltopdf(const ExportPdfOption &p_pdfOption, const QString &p_outputFile, const QUrl &p_baseUrl);
QSize pageLayoutSize(const QPageLayout &p_layout) const;
bool doWkhtmltopdf(const ExportPdfOption &p_pdfOption, const QStringList &p_htmlFiles, const QString &p_outputFile);
void prepareWkhtmltopdfArguments(const ExportPdfOption &p_pdfOption);
bool startProcess(const QString &p_program, const QStringList &p_args);
bool m_askedToStop = false;
bool m_exportOngoing = false;
WebViewStates m_webViewStates = WebViewState::Started;
// Managed by QObject.
MarkdownViewer *m_viewer = nullptr;
QString m_htmlTemplate;
QString m_exportHtmlTemplate;
QStringList m_wkhtmltopdfArgs;
};
}
Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::WebViewExporter::WebViewStates)
#endif // WEBVIEWEXPORTER_H

View File

@ -42,6 +42,8 @@ include($$LIBS_FOLDER/vtextedit/src/libs/syntax-highlighting/syntax-highlighting
include($$PWD/utils/utils.pri)
include($$PWD/export/export.pri)
include($$PWD/core/core.pri)
include($$PWD/widgets/widgets.pri)

View File

@ -1,4 +1,4 @@
#include "nodecontentmediautils.h"
#include "contentmediautils.h"
#include <QDebug>
#include <QSet>
@ -15,46 +15,53 @@
#include <utils/pathutils.h>
#include <utils/fileutils.h>
#include <core/file.h>
using namespace vnotex;
void NodeContentMediaUtils::copyMediaFiles(const Node *p_node,
INotebookBackend *p_backend,
const QString &p_destFilePath)
void ContentMediaUtils::copyMediaFiles(Node *p_node,
INotebookBackend *p_backend,
const QString &p_destFilePath)
{
Q_ASSERT(p_node->hasContent());
/*
const auto &fileType = FileTypeHelper::getInst().getFileType(p_node->fetchAbsolutePath());
if (fileType.m_type == FileTypeHelper::Markdown) {
copyMarkdownMediaFiles(p_node->read(),
PathUtils::parentDirPath(p_node->fetchContentPath()),
auto file = p_node->getContentFile();
if (file->getContentType().isMarkdown()) {
copyMarkdownMediaFiles(file->read(),
PathUtils::parentDirPath(file->getContentPath()),
p_backend,
p_destFilePath);
}
*/
}
void NodeContentMediaUtils::copyMediaFiles(const QString &p_filePath,
INotebookBackend *p_backend,
const QString &p_destFilePath)
void ContentMediaUtils::copyMediaFiles(const QString &p_filePath,
INotebookBackend *p_backend,
const QString &p_destFilePath)
{
/*
const auto &fileType = FileTypeHelper::getInst().getFileType(p_filePath);
if (fileType.m_type == FileTypeHelper::Markdown) {
if (fileType.isMarkdown()) {
copyMarkdownMediaFiles(FileUtils::readTextFile(p_filePath),
PathUtils::parentDirPath(p_filePath),
p_backend,
p_destFilePath);
}
*/
}
void NodeContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content,
const QString &p_basePath,
INotebookBackend *p_backend,
const QString &p_destFilePath)
void ContentMediaUtils::copyMediaFiles(const File *p_file,
const QString &p_destFilePath)
{
if (p_file->getContentType().isMarkdown()) {
copyMarkdownMediaFiles(p_file->read(),
p_file->getResourcePath(),
nullptr,
p_destFilePath);
}
}
void ContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content,
const QString &p_basePath,
INotebookBackend *p_backend,
const QString &p_destFilePath)
{
/*
auto content = p_content;
// Images.
@ -82,19 +89,20 @@ void NodeContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content,
handledImages.insert(link.m_path);
if (!QFileInfo::exists(link.m_path)) {
qWarning() << "Image of Markdown file does not exist" << link.m_path << link.m_urlInLink;
qWarning() << "image of Markdown file does not exist" << link.m_path << link.m_urlInLink;
continue;
}
// Get the relative path of the image and apply it to the dest file path.
const auto oldDestFilePath = destDir.filePath(link.m_urlInLink);
destDir.mkpath(PathUtils::parentDirPath(oldDestFilePath));
auto destFilePath = p_backend->renameIfExistsCaseInsensitive(oldDestFilePath);
auto destFilePath = p_backend ? p_backend->renameIfExistsCaseInsensitive(oldDestFilePath)
: FileUtils::renameIfExistsCaseInsensitive(oldDestFilePath);
if (oldDestFilePath != destFilePath) {
// Rename happens.
const auto oldFileName = PathUtils::fileName(oldDestFilePath);
const auto newFileName = PathUtils::fileName(destFilePath);
qWarning() << QString("Image name conflicts when copy. Renamed from (%1) to (%2)").arg(oldFileName, newFileName);
qWarning() << QString("image name conflicts when copy. Renamed from (%1) to (%2)").arg(oldFileName, newFileName);
// Update the text content.
auto newUrlInLink(link.m_urlInLink);
@ -106,38 +114,41 @@ void NodeContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content,
renamedImages.insert(link.m_path, newUrlInLink);
}
p_backend->copyFile(link.m_path, destFilePath);
if (p_backend) {
p_backend->copyFile(link.m_path, destFilePath);
} else {
FileUtils::copyFile(link.m_path, destFilePath);
}
}
if (!renamedImages.isEmpty()) {
p_backend->writeFile(p_destFilePath, content);
if (p_backend) {
p_backend->writeFile(p_destFilePath, content);
} else {
FileUtils::writeFile(p_destFilePath, content);
}
}
*/
}
void NodeContentMediaUtils::removeMediaFiles(const Node *p_node)
void ContentMediaUtils::removeMediaFiles(Node *p_node)
{
/*
Q_ASSERT(p_node->getType() == Node::Type::File);
const auto &fileType = FileTypeHelper::getInst().getFileType(p_node->fetchAbsolutePath());
if (fileType.m_type == FileTypeHelper::Markdown) {
removeMarkdownMediaFiles(p_node);
Q_ASSERT(p_node->hasContent());
auto file = p_node->getContentFile();
if (file->getContentType().isMarkdown()) {
removeMarkdownMediaFiles(file.data(), p_node->getBackend());
}
*/
}
void NodeContentMediaUtils::removeMarkdownMediaFiles(const Node *p_node)
void ContentMediaUtils::removeMarkdownMediaFiles(const File *p_file, INotebookBackend *p_backend)
{
/*
auto content = p_node->read();
auto content = p_file->read();
// Images.
const auto images =
vte::MarkdownUtils::fetchImagesFromMarkdownText(content,
PathUtils::parentDirPath(p_node->fetchContentPath()),
p_file->getResourcePath(),
vte::MarkdownLink::TypeFlag::LocalRelativeInternal);
auto backend = p_node->getBackend();
QSet<QString> handledImages;
for (const auto &link : images) {
if (handledImages.contains(link.m_path)) {
@ -150,40 +161,42 @@ void NodeContentMediaUtils::removeMarkdownMediaFiles(const Node *p_node)
qWarning() << "Image of Markdown file does not exist" << link.m_path << link.m_urlInLink;
continue;
}
backend->removeFile(link.m_path);
p_backend->removeFile(link.m_path);
}
*/
}
void NodeContentMediaUtils::copyAttachment(Node *p_node,
INotebookBackend *p_backend,
const QString &p_destFilePath,
const QString &p_destAttachmentFolderPath)
void ContentMediaUtils::copyAttachment(Node *p_node,
INotebookBackend *p_backend,
const QString &p_destFilePath,
const QString &p_destAttachmentFolderPath)
{
/*
Q_ASSERT(p_node->getType() == Node::Type::File);
Q_ASSERT(p_node->hasContent());
Q_ASSERT(!p_node->getAttachmentFolder().isEmpty());
// Copy the whole folder.
const auto srcAttachmentFolderPath = p_node->fetchAttachmentFolderPath();
p_backend->copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath);
if (p_backend) {
p_backend->copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath);
} else {
FileUtils::copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath);
}
// Check if we need to modify links in content.
// FIXME: check the whole relative path.
if (p_node->getAttachmentFolder() == PathUtils::dirName(p_destAttachmentFolderPath)) {
return;
}
const auto &fileType = FileTypeHelper::getInst().getFileType(p_node->fetchAbsolutePath());
if (fileType.m_type == FileTypeHelper::Markdown) {
auto file = p_node->getContentFile();
if (file->getContentType().isMarkdown()) {
fixMarkdownLinks(srcAttachmentFolderPath, p_backend, p_destFilePath, p_destAttachmentFolderPath);
}
*/
}
void NodeContentMediaUtils::fixMarkdownLinks(const QString &p_srcFolderPath,
INotebookBackend *p_backend,
const QString &p_destFilePath,
const QString &p_destFolderPath)
void ContentMediaUtils::fixMarkdownLinks(const QString &p_srcFolderPath,
INotebookBackend *p_backend,
const QString &p_destFilePath,
const QString &p_destFolderPath)
{
// TODO.
Q_UNUSED(p_srcFolderPath);

View File

@ -1,5 +1,5 @@
#ifndef NODECONTENTMEDIAUTILS_H
#define NODECONTENTMEDIAUTILS_H
#ifndef CONTENTMEDIAUTILS_H
#define CONTENTMEDIAUTILS_H
#include <QString>
@ -7,16 +7,17 @@ namespace vnotex
{
class INotebookBackend;
class Node;
class File;
// Utils to operate on the media files from node's content.
class NodeContentMediaUtils
class ContentMediaUtils
{
public:
NodeContentMediaUtils() = delete;
ContentMediaUtils() = delete;
// Fetch media files from @p_node and copy them to dest folder.
// @p_destFilePath: @p_node has been copied to @p_destFilePath.
static void copyMediaFiles(const Node *p_node,
static void copyMediaFiles(Node *p_node,
INotebookBackend *p_backend,
const QString &p_destFilePath);
@ -25,7 +26,10 @@ namespace vnotex
INotebookBackend *p_backend,
const QString &p_destFilePath);
static void removeMediaFiles(const Node *p_node);
static void copyMediaFiles(const File *p_file,
const QString &p_destFilePath);
static void removeMediaFiles(Node *p_node);
// Copy attachment folder.
static void copyAttachment(Node *p_node,
@ -39,7 +43,7 @@ namespace vnotex
INotebookBackend *p_backend,
const QString &p_destFilePath);
static void removeMarkdownMediaFiles(const Node *p_node);
static void removeMarkdownMediaFiles(const File *p_file, INotebookBackend *p_backend);
// Fix local relative internal links locating in @p_srcFolderPath.
static void fixMarkdownLinks(const QString &p_srcFolderPath,
@ -49,4 +53,4 @@ namespace vnotex
};
}
#endif // NODECONTENTMEDIAUTILS_H
#endif // CONTENTMEDIAUTILS_H

View File

@ -63,7 +63,7 @@ namespace vnotex
static QString generateFileNameWithSequence(const QString &p_folderPath,
const QString &p_baseName,
const QString &p_suffix);
const QString &p_suffix = QString());
static QTemporaryFile *createTemporaryFile(const QString &p_suffix);

View File

@ -10,3 +10,9 @@ bool HtmlUtils::hasOnlyImgTag(const QString &p_html)
QRegExp reg(QStringLiteral("<(?:p|span|div) "));
return !p_html.contains(reg);
}
QString HtmlUtils::escapeHtml(QString p_text)
{
p_text.replace(">", "&gt;").replace("<", "&lt;").replace("&", "&amp;");
return p_text;
}

View File

@ -11,6 +11,8 @@ namespace vnotex
HtmlUtils() = delete;
static bool hasOnlyImgTag(const QString &p_html);
static QString escapeHtml(QString p_text);
};
}

158
src/utils/processutils.cpp Normal file
View File

@ -0,0 +1,158 @@
#include "processutils.h"
#include <QProcess>
#include <QScopedPointer>
#include <QDebug>
#include "utils.h"
using namespace vnotex;
ProcessUtils::State ProcessUtils::start(const QString &p_program,
const QStringList &p_args,
const QByteArray &p_stdIn,
int &p_exitCodeOnSuccess,
QByteArray &p_stdOut,
QByteArray &p_stdErr)
{
QScopedPointer<QProcess> proc(new QProcess());
proc->start(p_program, p_args);
return handleProcess(proc.data(), p_stdIn, p_exitCodeOnSuccess, p_stdOut, p_stdErr);
}
ProcessUtils::State ProcessUtils::handleProcess(QProcess *p_process,
const QByteArray &p_stdIn,
int &p_exitCodeOnSuccess,
QByteArray &p_stdOut,
QByteArray &p_stdErr)
{
if (!p_process->waitForStarted()) {
return State::FailedToStart;
}
if (!p_stdIn.isEmpty()) {
if (p_process->write(p_stdIn) == -1) {
p_process->closeWriteChannel();
qWarning() << "failed to write to stdin of QProcess" << p_process->errorString();
return State::FailedToWrite;
} else {
p_process->closeWriteChannel();
}
}
p_process->waitForFinished();
State state = State::Succeeded;
if (p_process->exitStatus() == QProcess::CrashExit) {
state = State::Crashed;
} else {
p_exitCodeOnSuccess = p_process->exitCode();
}
p_stdOut = p_process->readAllStandardOutput();
p_stdErr = p_process->readAllStandardError();
return state;
}
QStringList ProcessUtils::parseCombinedArgString(const QString &p_args)
{
QStringList args;
QString tmp;
int quoteCount = 0;
bool inQuote = false;
// Handle quoting.
// Tokens can be surrounded by double quotes "hello world".
// Three consecutive double quotes represent the quote character itself.
for (int i = 0; i < p_args.size(); ++i) {
if (p_args.at(i) == QLatin1Char('"')) {
++quoteCount;
if (quoteCount == 3) {
// Third consecutive quote.
quoteCount = 0;
tmp += p_args.at(i);
}
continue;
}
if (quoteCount) {
if (quoteCount == 1) {
inQuote = !inQuote;
}
quoteCount = 0;
}
if (!inQuote && p_args.at(i).isSpace()) {
if (!tmp.isEmpty()) {
args += tmp;
tmp.clear();
}
} else {
tmp += p_args.at(i);
}
}
if (!tmp.isEmpty()) {
args += tmp;
}
return args;
}
QString ProcessUtils::combineArgString(const QStringList &p_args)
{
QString argStr;
for (const auto &arg : p_args) {
QString tmp(arg);
tmp.replace("\"", "\"\"\"");
if (tmp.contains(' ')) {
tmp = '"' + tmp + '"';
}
if (argStr.isEmpty()) {
argStr = tmp;
} else {
argStr = argStr + ' ' + tmp;
}
}
return argStr;
}
ProcessUtils::State ProcessUtils::start(const QString &p_program,
const QStringList &p_args,
const std::function<void(const QString &)> &p_logger,
const bool &p_askedToStop)
{
QProcess proc;
proc.start(p_program, p_args);
if (!proc.waitForStarted()) {
return State::FailedToStart;
}
while (proc.state() != QProcess::NotRunning) {
Utils::sleepWait(100);
auto outBa = proc.readAllStandardOutput();
auto errBa = proc.readAllStandardError();
QString msg;
if (!outBa.isEmpty()) {
msg += QString::fromLocal8Bit(outBa);
}
if (!errBa.isEmpty()) {
msg += QString::fromLocal8Bit(errBa);
}
if (!msg.isEmpty()) {
p_logger(msg);
}
if (p_askedToStop) {
break;
}
}
return proc.exitStatus() == QProcess::NormalExit ? State::Succeeded : State::Crashed;
}

52
src/utils/processutils.h Normal file
View File

@ -0,0 +1,52 @@
#ifndef PROCESSUTILS_H
#define PROCESSUTILS_H
#include <functional>
#include <QStringList>
#include <QByteArray>
class QProcess;
namespace vnotex
{
class ProcessUtils
{
public:
enum State
{
Succeeded,
Crashed,
FailedToStart,
FailedToWrite
};
ProcessUtils() = delete;
static State start(const QString &p_program,
const QStringList &p_args,
const QByteArray &p_stdIn,
int &p_exitCodeOnSuccess,
QByteArray &p_stdOut,
QByteArray &p_stdErr);
static State start(const QString &p_program,
const QStringList &p_args,
const std::function<void(const QString &)> &p_logger,
const bool &p_askedToStop);
// Copied from QProcess code.
static QStringList parseCombinedArgString(const QString &p_args);
static QString combineArgString(const QStringList &p_args);
private:
static State handleProcess(QProcess *p_process,
const QByteArray &p_stdIn,
int &p_exitCodeOnSuccess,
QByteArray &p_stdOut,
QByteArray &p_stdErr);
};
}
#endif // PROCESSUTILS_H

View File

@ -75,13 +75,3 @@ QString TextUtils::unindentTextMultiLines(const QString &p_text)
return res;
}
QString TextUtils::purifyUrl(const QString &p_url)
{
int idx = p_url.indexOf('?');
if (idx > -1) {
return p_url.left(idx);
}
return p_url;
}

View File

@ -20,9 +20,6 @@ namespace vnotex
// Unindent multi-lines text according to the indentation of the first line.
static QString unindentTextMultiLines(const QString &p_text);
// Remove query in the url (?xxx).
static QString purifyUrl(const QString &p_url);
};
}

View File

@ -1,25 +1,31 @@
QT += widgets svg
SOURCES += \
$$PWD/contentmediautils.cpp \
$$PWD/docsutils.cpp \
$$PWD/htmlutils.cpp \
$$PWD/pathutils.cpp \
$$PWD/processutils.cpp \
$$PWD/textutils.cpp \
$$PWD/urldragdroputils.cpp \
$$PWD/utils.cpp \
$$PWD/fileutils.cpp \
$$PWD/iconutils.cpp \
$$PWD/webutils.cpp \
$$PWD/widgetutils.cpp \
$$PWD/clipboardutils.cpp
HEADERS += \
$$PWD/contentmediautils.h \
$$PWD/docsutils.h \
$$PWD/htmlutils.h \
$$PWD/pathutils.h \
$$PWD/processutils.h \
$$PWD/textutils.h \
$$PWD/urldragdroputils.h \
$$PWD/utils.h \
$$PWD/fileutils.h \
$$PWD/iconutils.h \
$$PWD/webutils.h \
$$PWD/widgetutils.h \
$$PWD/clipboardutils.h

103
src/utils/webutils.cpp Normal file
View File

@ -0,0 +1,103 @@
#include "webutils.h"
#include <QUrl>
#include <QFileInfo>
#include <QImageReader>
#include "fileutils.h"
#include "pathutils.h"
#include <vtextedit/networkutils.h>
#include <core/exception.h>
using namespace vnotex;
QString WebUtils::purifyUrl(const QString &p_url)
{
int idx = p_url.indexOf('?');
if (idx > -1) {
return p_url.left(idx);
}
return p_url;
}
QString WebUtils::toDataUri(const QUrl &p_url, bool p_keepTitle)
{
QString uri;
Q_ASSERT(!p_url.isRelative());
QString file = p_url.isLocalFile() ? p_url.toLocalFile() : p_url.toString();
const auto filePath = purifyUrl(file);
const QFileInfo finfo(filePath);
const QString suffix(finfo.suffix().toLower());
if (!QImageReader::supportedImageFormats().contains(suffix.toLatin1())) {
return uri;
}
QByteArray data;
if (p_url.scheme() == "https" || p_url.scheme() == "http") {
// Download it.
data = vte::Downloader::download(p_url);
} else if (finfo.exists()) {
data = FileUtils::readFile(filePath);
}
if (data.isEmpty()) {
return uri;
}
if (suffix == "svg") {
uri = QString("data:image/svg+xml;utf8,%1").arg(QString::fromUtf8(data));
uri.replace('\r', "").replace('\n', "");
// Using unescaped '#' characters in a data URI body is deprecated and
// will be removed in M68, around July 2018. Please use '%23' instead.
uri.replace("#", "%23");
// Escape "'" to avoid conflict with src='...' attribute.
uri.replace("'", "%27");
if (!p_keepTitle) {
// Remove <title>...</title>.
QRegExp reg("<title>.*</title>", Qt::CaseInsensitive);
uri.remove(reg);
}
} else {
uri = QString("data:image/%1;base64,%2").arg(suffix, QString::fromUtf8(data.toBase64()));
}
return uri;
}
QString WebUtils::copyResource(const QUrl &p_url, const QString &p_folder)
{
Q_ASSERT(!p_url.isRelative());
QDir dir(p_folder);
if (!dir.exists()) {
dir.mkpath(p_folder);
}
QString file = p_url.isLocalFile() ? p_url.toLocalFile() : p_url.toString();
QFileInfo finfo(file);
auto fileName = FileUtils::generateFileNameWithSequence(p_folder, finfo.completeBaseName(), finfo.suffix());
QString targetFile = dir.absoluteFilePath(fileName);
bool succ = true;
try {
if (p_url.scheme() == "https" || p_url.scheme() == "http") {
// Download it.
auto data = vte::Downloader::download(p_url);
if (!data.isEmpty()) {
FileUtils::writeFile(targetFile, data);
}
} else if (finfo.exists()) {
// Do a copy.
FileUtils::copyFile(file, targetFile, false);
}
} catch (Exception &p_e) {
Q_UNUSED(p_e);
succ = false;
}
return succ ? targetFile : QString();
}

24
src/utils/webutils.h Normal file
View File

@ -0,0 +1,24 @@
#ifndef WEBUTILS_H
#define WEBUTILS_H
#include <QString>
class QUrl;
namespace vnotex
{
class WebUtils
{
public:
WebUtils() = delete;
// Remove query in the url (?xxx).
static QString purifyUrl(const QString &p_url);
static QString toDataUri(const QUrl &p_url, bool p_keepTitle);
static QString copyResource(const QUrl &p_url, const QString &p_folder);
};
}
#endif // WEBUTILS_H

View File

@ -21,7 +21,6 @@
#include <QFontDatabase>
#include <QMenu>
#include <QDebug>
#include <QFormLayout>
#include <QLineEdit>
using namespace vnotex;
@ -356,18 +355,6 @@ void WidgetUtils::insertActionAfter(QMenu *p_menu, QAction *p_after, QAction *p_
}
}
QFormLayout *WidgetUtils::createFormLayout(QWidget *p_parent)
{
auto layout = new QFormLayout(p_parent);
#if defined(Q_OS_MACOS)
layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
layout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop);
#endif
return layout;
}
void WidgetUtils::selectBaseName(QLineEdit *p_lineEdit)
{
auto text = p_lineEdit->text();

View File

@ -17,7 +17,6 @@ class QScrollArea;
class QListView;
class QMenu;
class QShortcut;
class QFormLayout;
class QLineEdit;
namespace vnotex
@ -79,8 +78,6 @@ namespace vnotex
static void insertActionAfter(QMenu *p_menu, QAction *p_after, QAction *p_action);
static QFormLayout *createFormLayout(QWidget *p_parent = nullptr);
// Select the base name part of the line edit content.
static void selectBaseName(QLineEdit *p_lineEdit);

View File

@ -12,6 +12,7 @@
#include <utils/widgetutils.h>
#include "../propertydefs.h"
#include "../widgetsfactory.h"
using namespace vnotex;
@ -29,6 +30,11 @@ void Dialog::setCentralWidget(QWidget *p_widget)
m_layout->addWidget(m_centralWidget);
}
void Dialog::addBottomWidget(QWidget *p_widget)
{
m_layout->insertWidget(m_layout->indexOf(m_centralWidget) + 1, p_widget);
}
void Dialog::setDialogButtonBox(QDialogButtonBox::StandardButtons p_buttons,
QDialogButtonBox::StandardButton p_defaultButton)
{
@ -38,7 +44,8 @@ void Dialog::setDialogButtonBox(QDialogButtonBox::StandardButtons p_buttons,
m_dialogButtonBox = new QDialogButtonBox(p_buttons, this);
connect(m_dialogButtonBox, &QDialogButtonBox::accepted,
this, &Dialog::acceptedButtonClicked);
connect(m_dialogButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_dialogButtonBox, &QDialogButtonBox::rejected,
this, &Dialog::rejectedButtonClicked);
connect(m_dialogButtonBox, &QDialogButtonBox::clicked,
this, [this](QAbstractButton *p_button) {
switch (m_dialogButtonBox->buttonRole(p_button)) {
@ -80,13 +87,13 @@ void Dialog::setInformationText(const QString &p_text, InformationLevel p_level)
return;
}
m_infoTextEdit = new QPlainTextEdit(this);
m_infoTextEdit->setReadOnly(true);
m_infoTextEdit = WidgetsFactory::createPlainTextConsole(this);
m_infoTextEdit->setMaximumHeight(m_infoTextEdit->minimumSizeHint().height());
m_layout->insertWidget(1, m_infoTextEdit);
m_layout->insertWidget(m_layout->count() - 1, m_infoTextEdit);
}
m_infoTextEdit->setPlainText(p_text);
m_infoTextEdit->ensureCursorVisible();
const bool visible = !p_text.isEmpty();
const bool needResize = visible != m_infoTextEdit->isVisible();
@ -114,11 +121,34 @@ void Dialog::setInformationText(const QString &p_text, InformationLevel p_level)
}
}
void Dialog::appendInformationText(const QString &p_text)
{
if (!m_infoTextEdit) {
setInformationText(p_text);
} else {
m_infoTextEdit->appendPlainText(p_text);
m_infoTextEdit->moveCursor(QTextCursor::End);
m_infoTextEdit->ensureCursorVisible();
}
}
void Dialog::clearInformationText()
{
if (m_infoTextEdit) {
m_infoTextEdit->clear();
}
}
void Dialog::acceptedButtonClicked()
{
QDialog::accept();
}
void Dialog::rejectedButtonClicked()
{
QDialog::reject();
}
void Dialog::resetButtonClicked()
{
}

View File

@ -15,8 +15,6 @@ namespace vnotex
public:
explicit Dialog(QWidget *p_parent = nullptr, Qt::WindowFlags p_flags = Qt::WindowFlags());
virtual void setCentralWidget(QWidget *p_widget);
void setDialogButtonBox(QDialogButtonBox::StandardButtons p_buttons,
QDialogButtonBox::StandardButton p_defaultButton = QDialogButtonBox::NoButton);
@ -31,6 +29,10 @@ namespace vnotex
void setInformationText(const QString &p_text, InformationLevel p_level = InformationLevel::Info);
void appendInformationText(const QString &p_text);
void clearInformationText();
void setButtonEnabled(QDialogButtonBox::StandardButton p_button, bool p_enabled);
// Dialog has completed but just stay the GUI to let user know information.
@ -41,10 +43,17 @@ namespace vnotex
protected:
virtual void acceptedButtonClicked();
virtual void rejectedButtonClicked();
virtual void resetButtonClicked();
virtual void appliedButtonClicked();
virtual void setCentralWidget(QWidget *p_widget);
// Add @p_widget below the central widget.
virtual void addBottomWidget(QWidget *p_widget);
QBoxLayout *m_layout = nullptr;
QWidget *m_centralWidget = nullptr;

View File

@ -0,0 +1,679 @@
#include "exportdialog.h"
#include <QGroupBox>
#include <QFormLayout>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QComboBox>
#include <QPushButton>
#include <QCheckBox>
#include <QLineEdit>
#include <QProgressBar>
#include <QFileInfo>
#include <QFileDialog>
#include <QUrl>
#include <QPlainTextEdit>
#include <QCoreApplication>
#include <QPrinter>
#include <QPageSetupDialog>
#include <QPageLayout>
#include <notebook/notebook.h>
#include <notebook/node.h>
#include <buffer/buffer.h>
#include <widgets/widgetsfactory.h>
#include <core/thememgr.h>
#include <core/configmgr.h>
#include <core/sessionconfig.h>
#include <core/vnotex.h>
#include <utils/widgetutils.h>
#include <utils/fileutils.h>
#include <utils/pathutils.h>
#include <utils/clipboardutils.h>
#include <export/exporter.h>
using namespace vnotex;
ExportDialog::ExportDialog(Notebook *p_notebook,
Node *p_folder,
Buffer *p_buffer,
QWidget *p_parent)
: ScrollDialog(p_parent),
m_notebook(p_notebook),
m_folder(p_folder),
m_buffer(p_buffer)
{
setupUI();
initOptions();
restoreFields(m_option);
connect(this, &QDialog::finished,
this, [this]() {
saveFields(m_option);
ConfigMgr::getInst().getSessionConfig().setExportOption(m_option);
});
}
void ExportDialog::setupUI()
{
auto widget = new QWidget(this);
setCentralWidget(widget);
auto mainLayout = new QVBoxLayout(widget);
auto sourceBox = setupSourceGroup(widget);
mainLayout->addWidget(sourceBox);
auto targetBox = setupTargetGroup(widget);
mainLayout->addWidget(targetBox);
m_advancedGroupBox = setupAdvancedGroup(widget);
mainLayout->addWidget(m_advancedGroupBox);
m_progressBar = new QProgressBar(widget);
m_progressBar->setRange(0, 0);
m_progressBar->hide();
addBottomWidget(m_progressBar);
setupButtonBox();
setWindowTitle(tr("Export"));
}
QGroupBox *ExportDialog::setupSourceGroup(QWidget *p_parent)
{
auto box = new QGroupBox(tr("Source"), p_parent);
auto layout = WidgetsFactory::createFormLayout(box);
{
m_sourceComboBox = WidgetsFactory::createComboBox(box);
if (m_buffer) {
m_sourceComboBox->addItem(tr("Current Buffer (%1)").arg(m_buffer->getName()),
static_cast<int>(ExportSource::CurrentBuffer));
}
if (m_folder && m_folder->isContainer()) {
m_sourceComboBox->addItem(tr("Current Folder (%1)").arg(m_folder->getName()),
static_cast<int>(ExportSource::CurrentFolder));
}
if (m_notebook) {
m_sourceComboBox->addItem(tr("Current Notebook (%1)").arg(m_notebook->getName()),
static_cast<int>(ExportSource::CurrentNotebook));
}
layout->addRow(tr("Source:"), m_sourceComboBox);
}
{
// TODO: Source format filtering.
}
return box;
}
QString ExportDialog::getDefaultOutputDir() const
{
return PathUtils::concatenateFilePath(ConfigMgr::getDocumentOrHomePath(), tr("vnote_exports"));
}
QGroupBox *ExportDialog::setupTargetGroup(QWidget *p_parent)
{
auto box = new QGroupBox(tr("Target"), p_parent);
auto layout = WidgetsFactory::createFormLayout(box);
{
m_targetFormatComboBox = WidgetsFactory::createComboBox(box);
m_targetFormatComboBox->addItem(tr("Markdown"),
static_cast<int>(ExportFormat::Markdown));
m_targetFormatComboBox->addItem(tr("HTML"),
static_cast<int>(ExportFormat::HTML));
m_targetFormatComboBox->addItem(tr("PDF"),
static_cast<int>(ExportFormat::PDF));
m_targetFormatComboBox->addItem(tr("Custom"),
static_cast<int>(ExportFormat::Custom));
connect(m_targetFormatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this]() {
AdvancedSettings settings = AdvancedSettings::Max;
int format = m_targetFormatComboBox->currentData().toInt();
switch (format) {
case ExportFormat::HTML:
settings = AdvancedSettings::HTML;
break;
case ExportFormat::PDF:
settings = AdvancedSettings::PDF;
break;
default:
break;
}
showAdvancedSettings(settings);
});
layout->addRow(tr("Format:"), m_targetFormatComboBox);
}
{
m_transparentBgCheckBox = WidgetsFactory::createCheckBox(tr("Use transparent background"), box);
layout->addRow(m_transparentBgCheckBox);
}
{
const auto webStyles = VNoteX::getInst().getThemeMgr().getWebStyles();
m_renderingStyleComboBox = WidgetsFactory::createComboBox(box);
layout->addRow(tr("Rendering style:"), m_renderingStyleComboBox);
for (const auto &pa : webStyles) {
m_renderingStyleComboBox->addItem(pa.first, pa.second);
}
m_syntaxHighlightStyleComboBox = WidgetsFactory::createComboBox(box);
layout->addRow(tr("Syntax highlighting style:"), m_syntaxHighlightStyleComboBox);
for (const auto &pa : webStyles) {
m_syntaxHighlightStyleComboBox->addItem(pa.first, pa.second);
}
}
{
auto outputLayout = new QHBoxLayout();
m_outputDirLineEdit = WidgetsFactory::createLineEdit(box);
outputLayout->addWidget(m_outputDirLineEdit);
auto browseBtn = new QPushButton(tr("Browse"), box);
outputLayout->addWidget(browseBtn);
connect(browseBtn, &QPushButton::clicked,
this, [this]() {
QString initPath = getOutputDir();
if (!QFileInfo::exists(initPath)) {
initPath = getDefaultOutputDir();
}
QString dirPath = QFileDialog::getExistingDirectory(this,
tr("Select Export Output Directory"),
initPath,
QFileDialog::ShowDirsOnly
| QFileDialog::DontResolveSymlinks);
if (!dirPath.isEmpty()) {
m_outputDirLineEdit->setText(dirPath);
}
});
layout->addRow(tr("Output directory:"), outputLayout);
}
return box;
}
QGroupBox *ExportDialog::setupAdvancedGroup(QWidget *p_parent)
{
auto box = new QGroupBox(tr("Advanced"), p_parent);
auto layout = new QVBoxLayout(box);
m_advancedSettings.resize(AdvancedSettings::Max);
m_advancedSettings[AdvancedSettings::General] = setupGeneralAdvancedSettings(box);
layout->addWidget(m_advancedSettings[AdvancedSettings::General]);
return box;
}
QWidget *ExportDialog::setupGeneralAdvancedSettings(QWidget *p_parent)
{
QWidget *widget = new QWidget(p_parent);
auto layout = WidgetsFactory::createFormLayout(widget);
layout->setContentsMargins(0, 0, 0, 0);
{
m_recursiveCheckBox = WidgetsFactory::createCheckBox(tr("Process sub-folders"), widget);
layout->addRow(m_recursiveCheckBox);
}
{
m_exportAttachmentsCheckBox = WidgetsFactory::createCheckBox(tr("Export attachments"), widget);
layout->addRow(m_exportAttachmentsCheckBox);
}
return widget;
}
void ExportDialog::setupButtonBox()
{
setDialogButtonBox(QDialogButtonBox::Close);
auto box = getDialogButtonBox();
m_exportBtn = box->addButton(tr("Export"), QDialogButtonBox::ActionRole);
connect(m_exportBtn, &QPushButton::clicked,
this, &ExportDialog::startExport);
m_openDirBtn = box->addButton(tr("Open Directory"), QDialogButtonBox::ActionRole);
connect(m_openDirBtn, &QPushButton::clicked,
this, [this]() {
auto dir = getOutputDir();
if (!dir.isEmpty()) {
WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(dir));
}
});
m_copyContentBtn = box->addButton(tr("Copy Content"), QDialogButtonBox::ActionRole);
m_copyContentBtn->setToolTip(tr("Copy exported file content"));
m_copyContentBtn->setEnabled(false);
connect(m_copyContentBtn, &QPushButton::clicked,
this, [this]() {
if (m_exportedFile.isEmpty()) {
return;
}
const auto content = FileUtils::readTextFile(m_exportedFile);
if (!content.isEmpty()) {
ClipboardUtils::setTextToClipboard(content);
}
});
}
QString ExportDialog::getOutputDir() const
{
return m_outputDirLineEdit->text();
}
void ExportDialog::initOptions()
{
// Read it from config.
m_option = ConfigMgr::getInst().getSessionConfig().getExportOption();
const auto &theme = VNoteX::getInst().getThemeMgr().getCurrentTheme();
m_option.m_renderingStyleFile = theme.getFile(Theme::File::WebStyleSheet);
m_option.m_syntaxHighlightStyleFile = theme.getFile(Theme::File::HighlightStyleSheet);
if (m_option.m_outputDir.isEmpty()) {
m_option.m_outputDir = getDefaultOutputDir();
}
}
void ExportDialog::restoreFields(const ExportOption &p_option)
{
{
int idx = m_sourceComboBox->findData(static_cast<int>(p_option.m_source));
if (idx != -1) {
m_sourceComboBox->setCurrentIndex(idx);
}
}
{
int idx = m_targetFormatComboBox->findData(static_cast<int>(p_option.m_targetFormat));
if (idx != -1) {
m_targetFormatComboBox->setCurrentIndex(idx);
}
}
m_transparentBgCheckBox->setChecked(p_option.m_useTransparentBg);
{
int idx = m_renderingStyleComboBox->findData(p_option.m_renderingStyleFile);
if (idx != -1) {
m_renderingStyleComboBox->setCurrentIndex(idx);
}
}
{
int idx = m_syntaxHighlightStyleComboBox->findData(p_option.m_syntaxHighlightStyleFile);
if (idx != -1) {
m_syntaxHighlightStyleComboBox->setCurrentIndex(idx);
}
}
m_outputDirLineEdit->setText(p_option.m_outputDir);
m_recursiveCheckBox->setChecked(p_option.m_recursive);
m_exportAttachmentsCheckBox->setChecked(p_option.m_exportAttachments);
}
void ExportDialog::saveFields(ExportOption &p_option)
{
p_option.m_source = static_cast<ExportSource>(m_sourceComboBox->currentData().toInt());
p_option.m_targetFormat = static_cast<ExportFormat>(m_targetFormatComboBox->currentData().toInt());
p_option.m_useTransparentBg = m_transparentBgCheckBox->isChecked();
p_option.m_renderingStyleFile = m_renderingStyleComboBox->currentData().toString();
p_option.m_syntaxHighlightStyleFile = m_syntaxHighlightStyleComboBox->currentData().toString();
p_option.m_outputDir = getOutputDir();
p_option.m_recursive = m_recursiveCheckBox->isChecked();
p_option.m_exportAttachments = m_exportAttachmentsCheckBox->isChecked();
if (m_advancedSettings[AdvancedSettings::HTML]) {
saveFields(p_option.m_htmlOption);
}
if (m_advancedSettings[AdvancedSettings::PDF]) {
saveFields(p_option.m_pdfOption);
}
}
void ExportDialog::startExport()
{
if (m_exportOngoing) {
return;
}
// On start.
{
m_exportedFile.clear();
m_exportOngoing = true;
updateUIOnExport();
}
saveFields(m_option);
int ret = doExport(m_option);
appendLog(tr("%n file(s) exported", "", ret));
// On end.
{
m_exportOngoing = false;
updateUIOnExport();
}
}
void ExportDialog::rejectedButtonClicked()
{
if (m_exportOngoing) {
// Just cancel the export.
appendLog(tr("Cancelling the export."));
m_exporter->stop();
} else {
Dialog::rejectedButtonClicked();
}
}
void ExportDialog::appendLog(const QString &p_log)
{
appendInformationText(">>> " + p_log);
QCoreApplication::sendPostedEvents();
}
void ExportDialog::updateUIOnExport()
{
m_exportBtn->setEnabled(!m_exportOngoing);
if (m_exportOngoing) {
m_progressBar->setMaximum(0);
m_progressBar->show();
} else {
m_progressBar->hide();
}
m_copyContentBtn->setEnabled(!m_exportedFile.isEmpty());
}
int ExportDialog::doExport(ExportOption p_option)
{
// TODO: Check ExportOption.
int exportedFilesCount = 0;
switch (p_option.m_source) {
case ExportSource::CurrentBuffer:
{
Q_ASSERT(m_buffer);
const auto outputFile = getExporter()->doExport(p_option, m_buffer);
exportedFilesCount = outputFile.isEmpty() ? 0 : 1;
if (exportedFilesCount == 1 && p_option.m_targetFormat == ExportFormat::HTML) {
m_exportedFile = outputFile;
}
break;
}
case ExportSource::CurrentFolder:
{
Q_ASSERT(m_folder);
const auto outputFiles = getExporter()->doExport(p_option, m_folder);
exportedFilesCount = outputFiles.size();
break;
}
case ExportSource::CurrentNotebook:
{
Q_ASSERT(m_notebook);
const auto outputFiles = getExporter()->doExport(p_option, m_notebook);
exportedFilesCount = outputFiles.size();
break;
}
}
return exportedFilesCount;
}
Exporter *ExportDialog::getExporter()
{
if (!m_exporter) {
m_exporter = new Exporter(this);
connect(m_exporter, &Exporter::progressUpdated,
this, &ExportDialog::updateProgress);
connect(m_exporter, &Exporter::logRequested,
this, &ExportDialog::appendLog);
}
return m_exporter;
}
void ExportDialog::updateProgress(int p_val, int p_maximum)
{
m_progressBar->setMaximum(p_maximum);
m_progressBar->setValue(p_val);
}
QWidget *ExportDialog::getHtmlAdvancedSettings()
{
if (!m_advancedSettings[AdvancedSettings::HTML]) {
// Setup HTML advanced settings.
QWidget *widget = new QWidget(m_advancedGroupBox);
auto layout = WidgetsFactory::createFormLayout(widget);
layout->setContentsMargins(0, 0, 0, 0);
{
m_embedStylesCheckBox = WidgetsFactory::createCheckBox(tr("Embed styles"), widget);
layout->addRow(m_embedStylesCheckBox);
}
{
m_embedImagesCheckBox = WidgetsFactory::createCheckBox(tr("Embed images"), widget);
layout->addRow(m_embedImagesCheckBox);
}
{
m_completePageCheckBox = WidgetsFactory::createCheckBox(tr("Complete page"), widget);
m_completePageCheckBox->setToolTip(tr("Export the whole page along with images which may change the links structure"));
connect(m_completePageCheckBox, &QCheckBox::stateChanged,
this, [this](int p_state) {
bool checked = p_state == Qt::Checked;
m_embedImagesCheckBox->setEnabled(checked);
});
layout->addRow(m_completePageCheckBox);
}
{
m_useMimeHtmlFormatCheckBox = WidgetsFactory::createCheckBox(tr("Mime HTML format"), widget);
connect(m_useMimeHtmlFormatCheckBox, &QCheckBox::stateChanged,
this, [this](int p_state) {
bool checked = p_state == Qt::Checked;
m_embedStylesCheckBox->setEnabled(!checked);
m_completePageCheckBox->setEnabled(!checked);
});
// TODO: do not support MHTML for now.
m_useMimeHtmlFormatCheckBox->setEnabled(false);
layout->addRow(m_useMimeHtmlFormatCheckBox);
}
{
m_addOutlinePanelCheckBox = WidgetsFactory::createCheckBox(tr("Add outline panel"), widget);
layout->addRow(m_addOutlinePanelCheckBox);
}
m_advancedGroupBox->layout()->addWidget(widget);
m_advancedSettings[AdvancedSettings::HTML] = widget;
restoreFields(m_option.m_htmlOption);
}
return m_advancedSettings[AdvancedSettings::HTML];
}
void ExportDialog::showAdvancedSettings(AdvancedSettings p_settings)
{
for (int i = AdvancedSettings::General + 1; i < m_advancedSettings.size(); ++i) {
if (m_advancedSettings[i]) {
m_advancedSettings[i]->hide();
}
}
QWidget *widget = nullptr;
switch (p_settings) {
case AdvancedSettings::HTML:
widget = getHtmlAdvancedSettings();
break;
case AdvancedSettings::PDF:
widget = getPdfAdvancedSettings();
break;
default:
break;
}
if (widget) {
widget->show();
}
}
void ExportDialog::restoreFields(const ExportHtmlOption &p_option)
{
m_embedStylesCheckBox->setChecked(p_option.m_embedStyles);
m_embedImagesCheckBox->setChecked(p_option.m_embedImages);
m_completePageCheckBox->setChecked(p_option.m_completePage);
m_useMimeHtmlFormatCheckBox->setChecked(p_option.m_useMimeHtmlFormat);
m_addOutlinePanelCheckBox->setChecked(p_option.m_addOutlinePanel);
}
void ExportDialog::saveFields(ExportHtmlOption &p_option)
{
p_option.m_embedStyles = m_embedStylesCheckBox->isChecked();
p_option.m_embedImages = m_embedImagesCheckBox->isChecked();
p_option.m_completePage = m_completePageCheckBox->isChecked();
p_option.m_useMimeHtmlFormat = m_useMimeHtmlFormatCheckBox->isChecked();
p_option.m_addOutlinePanel = m_addOutlinePanelCheckBox->isChecked();
}
QWidget *ExportDialog::getPdfAdvancedSettings()
{
if (!m_advancedSettings[AdvancedSettings::PDF]) {
QWidget *widget = new QWidget(m_advancedGroupBox);
auto layout = WidgetsFactory::createFormLayout(widget);
layout->setContentsMargins(0, 0, 0, 0);
{
m_pageLayoutBtn = new QPushButton(tr("Settings"), widget);
connect(m_pageLayoutBtn, &QPushButton::clicked,
this, [this]() {
QPrinter printer;
printer.setPageLayout(*m_pageLayout);
QPageSetupDialog dlg(&printer, this);
if (dlg.exec() != QDialog::Accepted) {
return;
}
m_pageLayout->setUnits(QPageLayout::Millimeter);
m_pageLayout->setPageSize(printer.pageLayout().pageSize());
m_pageLayout->setMargins(printer.pageLayout().margins(QPageLayout::Millimeter));
m_pageLayout->setOrientation(printer.pageLayout().orientation());
updatePageLayoutButtonLabel();
});
layout->addRow(tr("Page layout:"), m_pageLayoutBtn);
}
{
m_addTableOfContentsCheckBox = WidgetsFactory::createCheckBox(tr("Add Table-Of-Contents"), widget);
layout->addRow(m_addTableOfContentsCheckBox);
}
{
auto useLayout = new QHBoxLayout();
m_useWkhtmltopdfCheckBox = WidgetsFactory::createCheckBox(tr("Use wkhtmltopdf"), widget);
useLayout->addWidget(m_useWkhtmltopdfCheckBox);
auto downloadBtn = new QPushButton(tr("Download"), widget);
connect(downloadBtn, &QPushButton::clicked,
this, []() {
WidgetUtils::openUrlByDesktop(QUrl("https://wkhtmltopdf.org/downloads.html"));
});
useLayout->addWidget(downloadBtn);
layout->addRow(useLayout);
}
{
auto pathLayout = new QHBoxLayout();
m_wkhtmltopdfExePathLineEdit = WidgetsFactory::createLineEdit(widget);
pathLayout->addWidget(m_wkhtmltopdfExePathLineEdit);
auto browseBtn = new QPushButton(tr("Browse"), widget);
pathLayout->addWidget(browseBtn);
connect(browseBtn, &QPushButton::clicked,
this, [this]() {
QString filePath = QFileDialog::getOpenFileName(this,
tr("Select wkhtmltopdf Executable"),
QCoreApplication::applicationDirPath());
if (!filePath.isEmpty()) {
m_wkhtmltopdfExePathLineEdit->setText(filePath);
}
});
layout->addRow(tr("Wkhtmltopdf path:"), pathLayout);
}
{
m_wkhtmltopdfArgsLineEdit = WidgetsFactory::createLineEdit(widget);
layout->addRow(tr("Wkhtmltopdf arguments:"), m_wkhtmltopdfArgsLineEdit);
}
m_advancedGroupBox->layout()->addWidget(widget);
m_advancedSettings[AdvancedSettings::PDF] = widget;
restoreFields(m_option.m_pdfOption);
}
return m_advancedSettings[AdvancedSettings::PDF];
}
void ExportDialog::restoreFields(const ExportPdfOption &p_option)
{
m_pageLayout = p_option.m_layout;
updatePageLayoutButtonLabel();
m_addTableOfContentsCheckBox->setChecked(p_option.m_addTableOfContents);
m_useWkhtmltopdfCheckBox->setChecked(p_option.m_useWkhtmltopdf);
m_wkhtmltopdfExePathLineEdit->setText(p_option.m_wkhtmltopdfExePath);
m_wkhtmltopdfArgsLineEdit->setText(p_option.m_wkhtmltopdfArgs);
}
void ExportDialog::saveFields(ExportPdfOption &p_option)
{
p_option.m_layout = m_pageLayout;
p_option.m_addTableOfContents = m_addTableOfContentsCheckBox->isChecked();
p_option.m_useWkhtmltopdf = m_useWkhtmltopdfCheckBox->isChecked();
p_option.m_wkhtmltopdfExePath = m_wkhtmltopdfExePathLineEdit->text();
p_option.m_wkhtmltopdfArgs = m_wkhtmltopdfArgsLineEdit->text();
}
void ExportDialog::updatePageLayoutButtonLabel()
{
Q_ASSERT(m_pageLayout);
m_pageLayoutBtn->setText(
QString("%1, %2").arg(m_pageLayout->pageSize().name(),
m_pageLayout->orientation() == QPageLayout::Portrait ? tr("Portrait") : tr("Landscape")));
}

View File

@ -0,0 +1,171 @@
#ifndef EXPORTDIALOG_H
#define EXPORTDIALOG_H
#include "scrolldialog.h"
#include <QSharedPointer>
#include <export/exportdata.h>
class QGroupBox;
class QPushButton;
class QComboBox;
class QCheckBox;
class QLineEdit;
class QProgressBar;
class QPlainTextEdit;
class QPageLayout;
namespace vnotex
{
class Notebook;
class Node;
class Buffer;
class Exporter;
class ExportDialog : public ScrollDialog
{
Q_OBJECT
public:
// Current notebook/folder/buffer.
ExportDialog(Notebook *p_notebook,
Node *p_folder,
Buffer *p_buffer,
QWidget *p_parent = nullptr);
protected:
void rejectedButtonClicked() Q_DECL_OVERRIDE;
private slots:
void updateProgress(int p_val, int p_maximum);
void appendLog(const QString &p_log);
private:
enum AdvancedSettings
{
General,
HTML,
PDF,
Max
};
void setupUI();
QGroupBox *setupSourceGroup(QWidget *p_parent);
QGroupBox *setupTargetGroup(QWidget *p_parent);
QGroupBox *setupAdvancedGroup(QWidget *p_parent);
QWidget *setupGeneralAdvancedSettings(QWidget *p_parent);
QWidget *getHtmlAdvancedSettings();
QWidget *getPdfAdvancedSettings();
void showAdvancedSettings(AdvancedSettings p_settings);
void setupButtonBox();
QString getOutputDir() const;
void initOptions();
void restoreFields(const ExportOption &p_option);
void saveFields(ExportOption &p_option);
void restoreFields(const ExportHtmlOption &p_option);
void saveFields(ExportHtmlOption &p_option);
void restoreFields(const ExportPdfOption &p_option);
void saveFields(ExportPdfOption &p_option);
void startExport();
void updateUIOnExport();
// Return exported files count.
int doExport(ExportOption p_option);
Exporter *getExporter();
QString getDefaultOutputDir() const;
void updatePageLayoutButtonLabel();
// Managed by QObject.
Exporter *m_exporter = nullptr;
Notebook *m_notebook = nullptr;
Node *m_folder = nullptr;
Buffer *m_buffer = nullptr;
// Last exported single file.
QString m_exportedFile;
bool m_exportOngoing = false;
QPushButton *m_exportBtn = nullptr;
QPushButton *m_openDirBtn = nullptr;
QPushButton *m_copyContentBtn = nullptr;
QComboBox *m_sourceComboBox = nullptr;
QComboBox *m_targetFormatComboBox = nullptr;
QCheckBox *m_transparentBgCheckBox = nullptr;
QComboBox *m_renderingStyleComboBox = nullptr;
QComboBox *m_syntaxHighlightStyleComboBox = nullptr;
QLineEdit *m_outputDirLineEdit = nullptr;
QProgressBar *m_progressBar = nullptr;
QGroupBox *m_advancedGroupBox = nullptr;
QVector<QWidget *> m_advancedSettings;
// General settings.
QCheckBox *m_recursiveCheckBox = nullptr;
QCheckBox *m_exportAttachmentsCheckBox = nullptr;
// HTML settings.
QCheckBox *m_embedStylesCheckBox = nullptr;
QCheckBox *m_embedImagesCheckBox = nullptr;
QCheckBox *m_completePageCheckBox = nullptr;
QCheckBox *m_useMimeHtmlFormatCheckBox = nullptr;
QCheckBox *m_addOutlinePanelCheckBox = nullptr;
// PDF settings.
QPushButton *m_pageLayoutBtn = nullptr;
QCheckBox *m_addTableOfContentsCheckBox = nullptr;
QCheckBox *m_useWkhtmltopdfCheckBox = nullptr;
QLineEdit *m_wkhtmltopdfExePathLineEdit = nullptr;
QLineEdit *m_wkhtmltopdfArgsLineEdit = nullptr;
QSharedPointer<QPageLayout> m_pageLayout;
ExportOption m_option;
};
}
#endif // EXPORTDIALOG_H

View File

@ -30,7 +30,7 @@ void FilePropertiesDialog::setupUI()
auto widget = new QWidget(this);
setCentralWidget(widget);
auto mainLayout = WidgetUtils::createFormLayout(widget);
auto mainLayout = WidgetsFactory::createFormLayout(widget);
mainLayout->setContentsMargins(0, 0, 0, 0);
const QFileInfo info(m_path);

View File

@ -35,7 +35,7 @@ FolderFilesFilterWidget::FolderFilesFilterWidget(QWidget *p_parent)
void FolderFilesFilterWidget::setupUI()
{
auto mainLayout = WidgetUtils::createFormLayout(this);
auto mainLayout = WidgetsFactory::createFormLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
{

View File

@ -30,7 +30,7 @@ void LinkInsertDialog::setupUI(const QString &p_title,
auto mainWidget = new QWidget(this);
setCentralWidget(mainWidget);
auto mainLayout = WidgetUtils::createFormLayout(mainWidget);
auto mainLayout = WidgetsFactory::createFormLayout(mainWidget);
m_linkTextEdit = WidgetsFactory::createLineEdit(p_linkText, mainWidget);
mainLayout->addRow(tr("&Text:"), m_linkTextEdit);

View File

@ -36,7 +36,7 @@ void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Flags p_newNodeFlag
const bool createMode = m_mode == Mode::Create;
const bool isNote = p_newNodeFlags & Node::Flag::Content;
m_mainLayout = WidgetUtils::createFormLayout(this);
m_mainLayout = WidgetsFactory::createFormLayout(this);
m_mainLayout->addRow(tr("Notebook:"),
new QLabel(p_parentNode->getNotebook()->getName(), this));
@ -84,7 +84,7 @@ void NodeInfoWidget::setupNameLineEdit(QWidget *p_parent)
const auto &fileType = FileTypeHelper::getInst().getFileTypeBySuffix(suffix);
typeName = fileType.m_typeName;
} else {
typeName = FileTypeHelper::getInst().getFileType(FileTypeHelper::Others).m_typeName;
typeName = FileTypeHelper::getInst().getFileType(FileType::Others).m_typeName;
}
int idx = m_fileTypeComboBox->findData(typeName);

View File

@ -44,7 +44,7 @@ void NotebookInfoWidget::setupUI()
QGroupBox *NotebookInfoWidget::setupBasicInfoGroupBox(QWidget *p_parent)
{
auto box = new QGroupBox(tr("Basic Information"), p_parent);
auto mainLayout = WidgetUtils::createFormLayout(box);
auto mainLayout = WidgetsFactory::createFormLayout(box);
{
setupNotebookTypeComboBox(box);
@ -131,7 +131,7 @@ QLayout *NotebookInfoWidget::setupNotebookRootFolderPath(QWidget *p_parent)
QGroupBox *NotebookInfoWidget::setupAdvancedInfoGroupBox(QWidget *p_parent)
{
auto box = new QGroupBox(tr("Advanced Information"), p_parent);
auto mainLayout = WidgetUtils::createFormLayout(box);
auto mainLayout = WidgetsFactory::createFormLayout(box);
{
setupConfigMgrComboBox(box);

View File

@ -33,6 +33,11 @@ void ScrollDialog::setCentralWidget(QWidget *p_widget)
m_scrollArea->setWidget(p_widget);
}
void ScrollDialog::addBottomWidget(QWidget *p_widget)
{
m_layout->insertWidget(m_layout->indexOf(m_scrollArea) + 1, p_widget);
}
void ScrollDialog::showEvent(QShowEvent *p_event)
{
QDialog::showEvent(p_event);

View File

@ -13,11 +13,13 @@ namespace vnotex
public:
ScrollDialog(QWidget *p_parent = nullptr, Qt::WindowFlags p_flags = Qt::WindowFlags());
void setCentralWidget(QWidget *p_widget) Q_DECL_OVERRIDE;
void resizeToHideScrollBarLater(bool p_vertical, bool p_horizontal);
protected:
void setCentralWidget(QWidget *p_widget) Q_DECL_OVERRIDE;
void addBottomWidget(QWidget *p_widget) Q_DECL_OVERRIDE;
void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE;
private:

View File

@ -20,7 +20,7 @@ AppearancePage::AppearancePage(QWidget *p_parent)
void AppearancePage::setupUI()
{
auto mainLayout = WidgetUtils::createFormLayout(this);
auto mainLayout = WidgetsFactory::createFormLayout(this);
{
const QString label(tr("System title bar"));

View File

@ -20,7 +20,7 @@ EditorPage::EditorPage(QWidget *p_parent)
void EditorPage::setupUI()
{
auto mainLayout = WidgetUtils::createFormLayout(this);
auto mainLayout = WidgetsFactory::createFormLayout(this);
{
m_autoSavePolicyComboBox = WidgetsFactory::createComboBox(this);

View File

@ -20,7 +20,7 @@ GeneralPage::GeneralPage(QWidget *p_parent)
void GeneralPage::setupUI()
{
auto mainLayout = WidgetUtils::createFormLayout(this);
auto mainLayout = WidgetsFactory::createFormLayout(this);
{
m_localeComboBox = WidgetsFactory::createComboBox(this);

View File

@ -125,7 +125,7 @@ QString MarkdownEditorPage::title() const
QGroupBox *MarkdownEditorPage::setupReadGroup()
{
auto box = new QGroupBox(tr("Read"), this);
auto layout = WidgetUtils::createFormLayout(box);
auto layout = WidgetsFactory::createFormLayout(box);
{
const QString label(tr("Constrain image width"));
@ -197,7 +197,7 @@ QGroupBox *MarkdownEditorPage::setupReadGroup()
QGroupBox *MarkdownEditorPage::setupEditGroup()
{
auto box = new QGroupBox(tr("Edit"), this);
auto layout = WidgetUtils::createFormLayout(box);
auto layout = WidgetsFactory::createFormLayout(box);
{
const QString label(tr("Insert file name as title"));
@ -245,7 +245,7 @@ QGroupBox *MarkdownEditorPage::setupEditGroup()
QGroupBox *MarkdownEditorPage::setupGeneralGroup()
{
auto box = new QGroupBox(tr("General"), this);
auto layout = WidgetUtils::createFormLayout(box);
auto layout = WidgetsFactory::createFormLayout(box);
{
auto sectionLayout = new QHBoxLayout();

View File

@ -23,7 +23,7 @@ TextEditorPage::TextEditorPage(QWidget *p_parent)
void TextEditorPage::setupUI()
{
auto mainLayout = WidgetUtils::createFormLayout(this);
auto mainLayout = WidgetsFactory::createFormLayout(this);
{
m_lineNumberComboBox = WidgetsFactory::createComboBox(this);

View File

@ -36,6 +36,7 @@
#include <utils/htmlutils.h>
#include <utils/widgetutils.h>
#include <utils/textutils.h>
#include <utils/webutils.h>
#include <core/exception.h>
#include <core/markdowneditorconfig.h>
#include <core/texteditorconfig.h>
@ -1075,7 +1076,7 @@ void MarkdownEditor::fetchImagesToLocalAndReplace(QString &p_text)
// Only handle absolute file path or network path.
QString srcImagePath;
QFileInfo info(TextUtils::purifyUrl(imageUrl));
QFileInfo info(WebUtils::purifyUrl(imageUrl));
// For network image.
QScopedPointer<QTemporaryFile> tmpFile;

View File

@ -94,6 +94,16 @@ void MarkdownViewerAdapter::setText(int p_revision,
}
}
void MarkdownViewerAdapter::setText(const QString &p_text)
{
m_revision = 0;
if (m_viewerReady) {
emit textUpdated(p_text);
} else {
m_pendingData.reset(new MarkdownData(p_text, -1, ""));
}
}
void MarkdownViewerAdapter::setReady(bool p_ready)
{
if (m_viewerReady == p_ready) {
@ -328,3 +338,21 @@ void MarkdownViewerAdapter::setFindText(const QString &p_text, int p_totalMatche
{
emit findTextReady(p_text, p_totalMatches, p_currentMatchIndex);
}
void MarkdownViewerAdapter::setWorkFinished()
{
emit workFinished();
}
void MarkdownViewerAdapter::saveContent()
{
emit contentRequested();
}
void MarkdownViewerAdapter::setSavedContent(const QString &p_headContent,
const QString &p_styleContent,
const QString &p_content,
const QString &p_bodyClassList)
{
emit contentReady(p_headContent, p_styleContent, p_content, p_bodyClassList);
}

View File

@ -100,6 +100,8 @@ namespace vnotex
const QString &p_text,
int p_lineNumber);
void setText(const QString &p_text);
void scrollToPosition(const Position &p_pos);
int getTopLineNumber() const;
@ -119,10 +121,14 @@ namespace vnotex
void findText(const QString &p_text, FindOptions p_options);
void saveContent();
// Functions to be called from web side.
public slots:
void setReady(bool p_ready);
void setWorkFinished();
// The line number at the top.
void setTopLineNumber(int p_lineNumber);
@ -161,6 +167,8 @@ namespace vnotex
void setFindText(const QString &p_text, int p_totalMatches, int p_currentMatchIndex);
void setSavedContent(const QString &p_headContent, const QString &p_styleContent, const QString &p_content, const QString &p_bodyClassList);
// Signals to be connected at web side.
signals:
// Current Markdown text is updated.
@ -194,6 +202,9 @@ namespace vnotex
void findTextRequested(const QString &p_text, const QJsonObject &p_options);
// Request to get the whole HTML content.
void contentRequested();
// Signals to be connected at cpp side.
signals:
void graphPreviewDataReady(const PreviewData &p_data);
@ -202,6 +213,9 @@ namespace vnotex
void viewerReady();
// All rendering work has finished.
void workFinished();
void headingsChanged();
void currentHeadingChanged();
@ -216,6 +230,11 @@ namespace vnotex
void findTextReady(const QString &p_text, int p_totalMatches, int p_currentMatchIndex);
void contentReady(const QString &p_headContent,
const QString &p_styleContent,
const QString &p_content,
const QString &p_bodyClassList);
private:
void scrollToLine(int p_lineNumber);

View File

@ -29,7 +29,7 @@
#include <core/coreconfig.h>
#include <core/events.h>
#include <core/fileopenparameters.h>
#include <widgets/dialogs/scrolldialog.h>
#include <widgets/dialogs/exportdialog.h>
#include "viewwindow.h"
#include "outlineviewer.h"
#include <utils/widgetutils.h>
@ -57,6 +57,9 @@ MainWindow::MainWindow(QWidget *p_parent)
// Note that no user interaction is possible in this state.
connect(qApp, &QCoreApplication::aboutToQuit,
this, &MainWindow::closeOnQuit);
connect(&VNoteX::getInst(), &VNoteX::exportRequested,
this, &MainWindow::exportNotes);
}
MainWindow::~MainWindow()
@ -582,3 +585,18 @@ void MainWindow::updateTabBarStyle()
tabBar->setDrawBase(false);
}
}
void MainWindow::exportNotes()
{
auto currentNotebook = m_notebookExplorer->currentNotebook().data();
auto viewWindow = m_viewArea->getCurrentViewWindow();
auto folderNode = m_notebookExplorer->currentExploredFolderNode();
if (folderNode && (folderNode->isRoot() || currentNotebook->isRecycleBinNode(folderNode))) {
folderNode = nullptr;
}
ExportDialog dialog(currentNotebook,
folderNode,
viewWindow ? viewWindow->getBuffer() : nullptr,
this);
dialog.exec();
}

View File

@ -72,6 +72,8 @@ namespace vnotex
void updateTabBarStyle();
void exportNotes();
private:
// Index in m_docks.
enum DockIndex

Some files were not shown because too many files have changed in this diff Show More