Browse Source

VMdEditor: support copy selected text as HTML

Le Tan 8 years ago
parent
commit
2f1971476d

+ 81 - 0
src/dialog/vcopytextashtmldialog.cpp

@@ -0,0 +1,81 @@
+#include "vcopytextashtmldialog.h"
+
+#include <QtWidgets>
+#include <QWebEngineView>
+#include <QClipboard>
+#include <QMimeData>
+#include <QApplication>
+
+#include "utils/vutils.h"
+#include "utils/vclipboardutils.h"
+#include "utils/vwebutils.h"
+#include "vconfigmanager.h"
+
+extern VConfigManager *g_config;
+
+VCopyTextAsHtmlDialog::VCopyTextAsHtmlDialog(const QString &p_text, QWidget *p_parent)
+    : QDialog(p_parent), m_text(p_text)
+{
+    setupUI();
+}
+
+void VCopyTextAsHtmlDialog::setupUI()
+{
+    QLabel *textLabel = new QLabel(tr("Text:"));
+    m_textEdit = new QPlainTextEdit(m_text);
+    m_textEdit->setReadOnly(true);
+    m_textEdit->setProperty("LineEdit", true);
+
+    m_htmlLabel = new QLabel(tr("HTML:"));
+    m_htmlViewer = VUtils::getWebEngineView();
+    m_htmlViewer->setContextMenuPolicy(Qt::NoContextMenu);
+    m_htmlViewer->setMinimumSize(600, 400);
+
+    m_infoLabel = new QLabel(tr("Converting text to HTML ..."));
+
+    m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok);
+    connect(m_btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+    m_btnBox->button(QDialogButtonBox::Ok)->setProperty("SpecialBtn", true);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addWidget(textLabel);
+    mainLayout->addWidget(m_textEdit);
+    mainLayout->addWidget(m_htmlLabel);
+    mainLayout->addWidget(m_htmlViewer);
+    mainLayout->addWidget(m_infoLabel);
+    mainLayout->addStretch();
+    mainLayout->addWidget(m_btnBox);
+
+    setLayout(mainLayout);
+    setWindowTitle(tr("Copy Text As HTML"));
+
+    setHtmlVisible(false);
+}
+
+void VCopyTextAsHtmlDialog::setHtmlVisible(bool p_visible)
+{
+    m_htmlLabel->setVisible(p_visible);
+    m_htmlViewer->setVisible(p_visible);
+}
+
+void VCopyTextAsHtmlDialog::setConvertedHtml(const QUrl &p_baseUrl,
+                                             const QString &p_html)
+{
+    QString html = QString("<html><body>%1</body></html>").arg(p_html);
+    m_htmlViewer->setHtml(html, p_baseUrl);
+    setHtmlVisible(true);
+
+    // Fix image source.
+    if (g_config->getFixImageSrcInWebWhenCopied()) {
+        VWebUtils::fixImageSrcInHtml(p_baseUrl, html);
+    }
+
+    QClipboard *clipboard = QApplication::clipboard();
+    QMimeData *data = new QMimeData();
+    data->setText(m_text);
+    data->setHtml(html);
+    VClipboardUtils::setMimeDataToClipboard(clipboard, data, QClipboard::Clipboard);
+
+    QTimer::singleShot(3000, this, &VCopyTextAsHtmlDialog::accept);
+    m_infoLabel->setText(tr("HTML has been copied. Will be closed in 3 seconds."));
+}

+ 46 - 0
src/dialog/vcopytextashtmldialog.h

@@ -0,0 +1,46 @@
+#ifndef VCOPYTEXTASHTMLDIALOG_H
+#define VCOPYTEXTASHTMLDIALOG_H
+
+#include <QDialog>
+#include <QUrl>
+
+
+class QPlainTextEdit;
+class QWebEngineView;
+class QDialogButtonBox;
+class VWaitingWidget;
+class QLabel;
+
+class VCopyTextAsHtmlDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    VCopyTextAsHtmlDialog(const QString &p_text, QWidget *p_parent = nullptr);
+
+    void setConvertedHtml(const QUrl &p_baseUrl, const QString &p_html);
+
+    const QString &getText() const;
+
+private:
+    void setupUI();
+
+    void setHtmlVisible(bool p_visible);
+
+    QPlainTextEdit *m_textEdit;
+
+    QLabel *m_htmlLabel;
+
+    QWebEngineView *m_htmlViewer;
+
+    QLabel *m_infoLabel;
+
+    QDialogButtonBox *m_btnBox;
+
+    QString m_text;
+};
+
+inline const QString &VCopyTextAsHtmlDialog::getText() const
+{
+    return m_text;
+}
+#endif // VCOPYTEXTASHTMLDIALOG_H

+ 12 - 0
src/resources/hoedown.js

@@ -69,3 +69,15 @@ var highlightText = function(text, id, timeStamp) {
     var html = marked(text);
     content.highlightTextCB(html, id, timeStamp);
 }
+
+var textToHtml = function(text) {
+    var html = marked(text);
+    var container = document.getElementById('text-to-html-div');
+    container.innerHTML = html;
+
+    html = getHtmlWithInlineStyles(container);
+
+    container.innerHTML = "";
+
+    content.textToHtmlCB(text, html);
+}

+ 11 - 0
src/resources/markdown-it.js

@@ -124,3 +124,14 @@ var highlightText = function(text, id, timeStamp) {
     content.highlightTextCB(html, id, timeStamp);
 }
 
+var textToHtml = function(text) {
+    var html = mdit.render(text);
+    var container = document.getElementById('text-to-html-div');
+    container.innerHTML = html;
+
+    html = getHtmlWithInlineStyles(container);
+
+    container.innerHTML = "";
+
+    content.textToHtmlCB(text, html);
+}

+ 2 - 0
src/resources/markdown_template.html

@@ -31,5 +31,7 @@
 </head>
 <body>
     <div id="placeholder"></div>
+
+    <div id="text-to-html-div" style="display:none;"></div>
 </body>
 </html>

+ 74 - 1
src/resources/markdown_template.js

@@ -28,6 +28,10 @@ if (typeof VEnableHighlightLineNumber == 'undefined') {
     VEnableHighlightLineNumber = false;
 }
 
+if (typeof VStylesToInline == 'undefined') {
+    VStylesToInline = '';
+}
+
 // Add a caption (using alt text) under the image.
 var VImageCenterClass = 'img-center';
 var VImageCaptionClass = 'img-caption';
@@ -53,6 +57,11 @@ new QWebChannel(qt.webChannelTransport,
             content.requestHighlightText.connect(highlightText);
             content.noticeReadyToHighlightText();
         }
+
+        if (typeof textToHtml == "function") {
+            content.requestTextToHtml.connect(textToHtml);
+            content.noticeReadyToTextToHtml();
+        }
     });
 
 var VHighlightedAnchorClass = 'highlighted-anchor';
@@ -853,4 +862,68 @@ var listContainsRegex = function(strs, exp) {
     }
 
     return false;
-}
+};
+
+var StylesToInline = null;
+
+var initStylesToInline = function() {
+    console.log('initStylesToInline');
+    StylesToInline = new Map();
+
+    if (VStylesToInline.length == 0) {
+        return;
+    }
+
+    var rules = VStylesToInline.split(',');
+    for (var i = 0; i < rules.length; ++i) {
+        var vals = rules[i].split('$');
+        if (vals.length != 2) {
+            continue;
+        }
+
+        var tags = vals[0].split(':');
+        var pros = vals[1].split(':');
+        for (var j = 0; j < tags.length; ++j) {
+            StylesToInline.set(tags[j].toLowerCase(), pros);
+        }
+    }
+};
+
+// Embed the CSS styles of @ele and all its children.
+var embedInlineStyles = function(ele) {
+    var tagName = ele.tagName.toLowerCase();
+    var props = StylesToInline.get(tagName);
+    if (!props) {
+        props = StylesToInline.get('all');
+
+        if (!props) {
+            return;
+        }
+    }
+
+    // Embed itself.
+    var style = window.getComputedStyle(ele, null);
+    for (var i = 0; i < props.length; ++i) {
+        var pro = props[i];
+        ele.style.setProperty(pro, style.getPropertyValue(pro));
+    }
+
+    // Embed children.
+    var children = ele.children;
+    for (var i = 0; i < children.length; ++i) {
+        embedInlineStyles(children[i]);
+    }
+};
+
+var getHtmlWithInlineStyles = function(container) {
+    if (!StylesToInline) {
+        initStylesToInline();
+    }
+
+    var children = container.children;
+    for (var i = 0; i < children.length; ++i) {
+        embedInlineStyles(children[i]);
+    }
+
+    return container.innerHTML;
+};

+ 11 - 0
src/resources/marked.js

@@ -75,3 +75,14 @@ var highlightText = function(text, id, timeStamp) {
     content.highlightTextCB(html, id, timeStamp);
 }
 
+var textToHtml = function(text) {
+    var html = marked(text);
+    var container = document.getElementById('text-to-html-div');
+    container.innerHTML = html;
+
+    html = getHtmlWithInlineStyles(container);
+
+    container.innerHTML = "";
+
+    content.textToHtmlCB(text, html);
+}

+ 20 - 0
src/resources/showdown.js

@@ -109,3 +109,23 @@ var highlightText = function(text, id, timeStamp) {
     content.highlightTextCB(html, id, timeStamp);
 }
 
+var textToHtml = function(text) {
+    var html = renderer.makeHtml(text);
+
+    var parser = new DOMParser();
+    var htmlDoc = parser.parseFromString("<div id=\"showdown-container\">" + html + "</div>", 'text/html');
+    highlightCodeBlocks(htmlDoc, false, false);
+
+    html = htmlDoc.getElementById('showdown-container').innerHTML;
+
+    delete parser;
+
+    var container = document.getElementById('text-to-html-div');
+    container.innerHTML = html;
+
+    html = getHtmlWithInlineStyles(container);
+
+    container.innerHTML = "";
+
+    content.textToHtmlCB(text, html);
+}

+ 9 - 0
src/resources/vnote.ini

@@ -196,6 +196,15 @@ mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.
 ; Fix local relative image source when copied
 fix_img_src_when_copied=true
 
+; Styles to be removed when copied in read mode
+; style1,style2,style3
+styles_to_remove_when_copied=margin,margin-left,margin-right,padding,padding-left,padding-right
+
+; CSS properties to embed as inline styles when copied in edit mode
+; tag1:tag2:tag3$property1:property2:property3,tag4:tag5$property2:property3
+; "all" for all tags not specified explicitly
+styles_to_inline_when_copied=all$border:color:display:font-family:font-size:font-style:white-space:word-spacing:line-height:text-align:text-indent:padding-top:padding-bottom:margin-top:margin-bottom,code$font-family:font-size:line-height:color:display:overfow-x,li$line-height,a$color:vertical-align,pre$display:overflow-y:overflow-x:color:font-size:font-style:font-weight:letter-spacing:text-align:text-indent:word-spacing
+
 [shortcuts]
 ; Define shortcuts here, with each item in the form "operation=keysequence".
 ; Leave keysequence empty to disable the shortcut of an operation.

+ 8 - 2
src/src.pro

@@ -102,7 +102,10 @@ SOURCES += main.cpp\
     vbuttonmenuitem.cpp \
     utils/viconutils.cpp \
     lineeditdelegate.cpp \
-    dialog/vtipsdialog.cpp
+    dialog/vtipsdialog.cpp \
+    dialog/vcopytextashtmldialog.cpp \
+    vwaitingwidget.cpp \
+    utils/vwebutils.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -191,7 +194,10 @@ HEADERS  += vmainwindow.h \
     vbuttonmenuitem.h \
     utils/viconutils.h \
     lineeditdelegate.h \
-    dialog/vtipsdialog.h
+    dialog/vtipsdialog.h \
+    dialog/vcopytextashtmldialog.h \
+    vwaitingwidget.h \
+    utils/vwebutils.h
 
 RESOURCES += \
     vnote.qrc \

+ 2 - 0
src/utils/vutils.cpp

@@ -664,6 +664,8 @@ QString VUtils::generateHtmlTemplate(MarkdownConverterType p_conType, bool p_exp
                      "<script>var VEnableHighlightLineNumber = true;</script>\n";
     }
 
+    extraFile += "<script>var VStylesToInline = '" + g_config->getStylesToInlineWhenCopied() + "';</script>\n";
+
     QString htmlTemplate;
     if (p_exportPdf) {
         htmlTemplate = VNote::s_markdownTemplatePDF;

+ 1 - 3
src/utils/vvim.cpp

@@ -6321,9 +6321,7 @@ void VVim::handleMouseMoved(QMouseEvent *p_event)
 
 void VVim::handleMouseReleased(QMouseEvent *p_event)
 {
-    Q_UNUSED(p_event);
-
-    if (checkMode(VimMode::Normal)) {
+    if (checkMode(VimMode::Normal) && p_event->button() == Qt::LeftButton) {
         QTextCursor cursor = m_editor->textCursorW();
         if (cursor.hasSelection()) {
             return;

+ 58 - 0
src/utils/vwebutils.cpp

@@ -0,0 +1,58 @@
+#include "vwebutils.h"
+
+#include <QRegExp>
+#include <QFileInfo>
+#include <QDebug>
+
+VWebUtils::VWebUtils()
+{
+}
+
+bool VWebUtils::fixImageSrcInHtml(const QUrl &p_baseUrl, QString &p_html)
+{
+    bool changed = false;
+
+#if defined(Q_OS_WIN)
+    QUrl::ComponentFormattingOption strOpt = QUrl::EncodeSpaces;
+#else
+    QUrl::ComponentFormattingOption strOpt = QUrl::FullyEncoded;
+#endif
+
+    QRegExp reg("(<img src=\")([^\"]+)\"");
+
+    int pos = 0;
+    while (pos < p_html.size()) {
+        int idx = p_html.indexOf(reg, pos);
+        if (idx == -1) {
+            break;
+        }
+
+        QString urlStr = reg.cap(2);
+        QUrl imgUrl(urlStr);
+
+        QString fixedStr;
+        if (imgUrl.isRelative()) {
+            fixedStr = p_baseUrl.resolved(imgUrl).toString(strOpt);
+        } else if (imgUrl.isLocalFile()) {
+            fixedStr = imgUrl.toString(strOpt);
+        } else if (imgUrl.scheme() != "https" && imgUrl.scheme() != "http") {
+            QString tmp = imgUrl.toString();
+            if (QFileInfo::exists(tmp)) {
+                fixedStr = QUrl::fromLocalFile(tmp).toString(strOpt);
+            }
+        }
+
+        pos = idx + reg.matchedLength();
+        if (!fixedStr.isEmpty() && urlStr != fixedStr) {
+            qDebug() << "fix img url" << urlStr << fixedStr;
+            // Insert one more space to avoid fix the url twice.
+            pos = pos + fixedStr.size() + 1 - urlStr.size();
+            p_html.replace(idx,
+                           reg.matchedLength(),
+                           QString("<img  src=\"%1\"").arg(fixedStr));
+            changed = true;
+        }
+    }
+
+    return changed;
+}

+ 18 - 0
src/utils/vwebutils.h

@@ -0,0 +1,18 @@
+#ifndef VWEBUTILS_H
+#define VWEBUTILS_H
+
+#include <QUrl>
+#include <QString>
+
+
+class VWebUtils
+{
+public:
+    // Fix <img src> in @p_html.
+    static bool fixImageSrcInHtml(const QUrl &p_baseUrl, QString &p_html);
+
+private:
+    VWebUtils();
+};
+
+#endif // VWEBUTILS_H

+ 6 - 0
src/vconfigmanager.cpp

@@ -287,6 +287,12 @@ void VConfigManager::initialize()
 
     m_fixImageSrcInWebWhenCopied = getConfigFromSettings("web",
                                                          "fix_img_src_when_copied").toBool();
+
+    m_stylesToRemoveWhenCopied = getConfigFromSettings("web",
+                                                       "styles_to_remove_when_copied").toStringList();
+
+    m_stylesToInlineWhenCopied = getConfigFromSettings("web",
+                                                       "styles_to_inline_when_copied").toStringList().join(",");
 }
 
 void VConfigManager::initSettings()

+ 19 - 0
src/vconfigmanager.h

@@ -428,6 +428,10 @@ public:
 
     bool getFixImageSrcInWebWhenCopied() const;
 
+    const QStringList &getStylesToRemoveWhenCopied() const;
+
+    const QString &getStylesToInlineWhenCopied() const;
+
 private:
     // Look up a config from user and default settings.
     QVariant getConfigFromSettings(const QString &section, const QString &key) const;
@@ -821,6 +825,12 @@ private:
     // Whether fix the local relative image src in read mode when copied.
     bool m_fixImageSrcInWebWhenCopied;
 
+    // Styles to be removed when copied in read mode.
+    QStringList m_stylesToRemoveWhenCopied;
+
+    // The string containing styles to inline when copied in edit mode.
+    QString m_stylesToInlineWhenCopied;
+
     // The name of the config file in each directory, obsolete.
     // Use c_dirConfigFile instead.
     static const QString c_obsoleteDirConfigFile;
@@ -1993,4 +2003,13 @@ inline bool VConfigManager::getFixImageSrcInWebWhenCopied() const
     return m_fixImageSrcInWebWhenCopied;
 }
 
+inline const QStringList &VConfigManager::getStylesToRemoveWhenCopied() const
+{
+    return m_stylesToRemoveWhenCopied;
+}
+
+inline const QString &VConfigManager::getStylesToInlineWhenCopied() const
+{
+    return m_stylesToInlineWhenCopied;
+}
 #endif // VCONFIGMANAGER_H

+ 15 - 0
src/vdocument.cpp

@@ -77,12 +77,27 @@ void VDocument::highlightTextCB(const QString &p_html, int p_id, int p_timeStamp
     emit textHighlighted(p_html, p_id, p_timeStamp);
 }
 
+void VDocument::textToHtmlAsync(const QString &p_text)
+{
+    emit requestTextToHtml(p_text);
+}
+
+void VDocument::textToHtmlCB(const QString &p_text, const QString &p_html)
+{
+    emit textToHtmlFinished(p_text, p_html);
+}
+
 void VDocument::noticeReadyToHighlightText()
 {
     m_readyToHighlight = true;
     emit readyToHighlightText();
 }
 
+void VDocument::noticeReadyToTextToHtml()
+{
+    m_readyToTextToHtml = true;
+}
+
 void VDocument::setFile(const VFile *p_file)
 {
     m_file = p_file;

+ 30 - 0
src/vdocument.h

@@ -29,10 +29,15 @@ public:
     // Use p_id to identify the result.
     void highlightTextAsync(const QString &p_text, int p_id, int p_timeStamp);
 
+    // Request to convert @p_text to HTML.
+    void textToHtmlAsync(const QString &p_text);
+
     void setFile(const VFile *p_file);
 
     bool isReadyToHighlight() const;
 
+    bool isReadyToTextToHtml() const;
+
 public slots:
     // Will be called in the HTML side
 
@@ -49,9 +54,15 @@ public slots:
     void setLog(const QString &p_log);
     void keyPressEvent(int p_key, bool p_ctrl, bool p_shift);
     void updateText();
+
     void highlightTextCB(const QString &p_html, int p_id, int p_timeStamp);
+
     void noticeReadyToHighlightText();
 
+    void textToHtmlCB(const QString &p_text, const QString &p_html);
+
+    void noticeReadyToTextToHtml();
+
     // Web-side handle logics (MathJax etc.) is finished.
     // But the page may not finish loading, such as images.
     void finishLogics();
@@ -65,14 +76,25 @@ signals:
 
     // @anchor is the id of that anchor, without '#'.
     void headerChanged(const QString &anchor);
+
     void htmlChanged(const QString &html);
+
     void logChanged(const QString &p_log);
+
     void keyPressed(int p_key, bool p_ctrl, bool p_shift);
+
     void requestHighlightText(const QString &p_text, int p_id, int p_timeStamp);
+
     void textHighlighted(const QString &p_html, int p_id, int p_timeStamp);
+
     void readyToHighlightText();
+
     void logicsFinished();
 
+    void requestTextToHtml(const QString &p_text);
+
+    void textToHtmlFinished(const QString &p_text, const QString &p_html);
+
 private:
     QString m_toc;
     QString m_header;
@@ -87,10 +109,18 @@ private:
 
     // Whether the web side is ready to handle highlight text request.
     bool m_readyToHighlight;
+
+    // Whether the web side is ready to convert text to html.
+    bool m_readyToTextToHtml;
 };
 
 inline bool VDocument::isReadyToHighlight() const
 {
     return m_readyToHighlight;
 }
+
+inline bool VDocument::isReadyToTextToHtml() const
+{
+    return m_readyToTextToHtml;
+}
 #endif // VDOCUMENT_H

+ 48 - 9
src/vmdeditor.cpp

@@ -19,6 +19,7 @@
 #include "vnotefile.h"
 #include "vpreviewmanager.h"
 #include "utils/viconutils.h"
+#include "dialog/vcopytextashtmldialog.h"
 
 extern VConfigManager *g_config;
 
@@ -29,7 +30,8 @@ VMdEditor::VMdEditor(VFile *p_file,
     : VTextEdit(p_parent),
       VEditor(p_file, this),
       m_mdHighlighter(NULL),
-      m_freshEdit(true)
+      m_freshEdit(true),
+      m_textToHtmlDialog(NULL)
 {
     Q_ASSERT(p_file->getDocType() == DocType::Markdown);
 
@@ -266,12 +268,19 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
     QMenu *menu = createStandardContextMenu();
     menu->setToolTipsVisible(true);
 
-    const QList<QAction *> actions = menu->actions();
+    VEditTab *editTab = dynamic_cast<VEditTab *>(parent());
+    Q_ASSERT(editTab);
+    if (editTab->isEditMode()) {
+        const QList<QAction *> actions = menu->actions();
 
-    if (!textCursor().hasSelection()) {
-        VEditTab *editTab = dynamic_cast<VEditTab *>(parent());
-        Q_ASSERT(editTab);
-        if (editTab->isEditMode()) {
+        if (textCursor().hasSelection()) {
+            QAction *copyAsHtmlAct = new QAction(tr("Copy As &HTML without Background"), menu);
+            copyAsHtmlAct->setToolTip(tr("Copy selected contents as HTML without background styles"));
+            connect(copyAsHtmlAct, &QAction::triggered,
+                    this, &VMdEditor::handleCopyAsHtmlAction);
+
+            menu->insertAction(actions.isEmpty() ? NULL : actions[0], copyAsHtmlAct);
+        } else {
             QAction *saveExitAct = new QAction(VIconUtils::menuIcon(":/resources/icons/save_exit.svg"),
                                                tr("&Save Changes And Read"),
                                                menu);
@@ -292,9 +301,10 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
 
             menu->insertAction(actions.isEmpty() ? NULL : actions[0], discardExitAct);
             menu->insertAction(discardExitAct, saveExitAct);
-            if (!actions.isEmpty()) {
-                menu->insertSeparator(actions[0]);
-            }
+        }
+
+        if (!actions.isEmpty()) {
+            menu->insertSeparator(actions[0]);
         }
     }
 
@@ -990,3 +1000,32 @@ void VMdEditor::updateInitAndInsertedImages(bool p_fileChanged, UpdateAction p_a
         }
     }
 }
+
+void VMdEditor::handleCopyAsHtmlAction()
+{
+    QTextCursor cursor = textCursor();
+    Q_ASSERT(cursor.hasSelection());
+
+    QString text = VEditUtils::selectedText(cursor);
+    Q_ASSERT(!text.isEmpty());
+
+    Q_ASSERT(!m_textToHtmlDialog);
+    m_textToHtmlDialog = new VCopyTextAsHtmlDialog(text, this);
+
+    // For Hoedown, we use marked.js to convert the text to have a general interface.
+    emit requestTextToHtml(text);
+
+    m_textToHtmlDialog->exec();
+
+    delete m_textToHtmlDialog;
+    m_textToHtmlDialog = NULL;
+}
+
+void VMdEditor::textToHtmlFinished(const QString &p_text,
+                                   const QUrl &p_baseUrl,
+                                   const QString &p_html)
+{
+    if (m_textToHtmlDialog && m_textToHtmlDialog->getText() == p_text) {
+        m_textToHtmlDialog->setConvertedHtml(p_baseUrl, p_html);
+    }
+}

+ 12 - 0
src/vmdeditor.h

@@ -5,6 +5,7 @@
 #include <QString>
 #include <QClipboard>
 #include <QImage>
+#include <QUrl>
 
 #include "vtextedit.h"
 #include "veditor.h"
@@ -17,6 +18,7 @@ class HGMarkdownHighlighter;
 class VCodeBlockHighlightHelper;
 class VDocument;
 class VPreviewManager;
+class VCopyTextAsHtmlDialog;
 
 class VMdEditor : public VTextEdit, public VEditor
 {
@@ -68,6 +70,8 @@ public:
 public slots:
     bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE;
 
+    void textToHtmlFinished(const QString &p_text, const QUrl &p_baseUrl, const QString &p_html);
+
 // Wrapper functions for QPlainTextEdit/QTextEdit.
 public:
     void setExtraSelectionsW(const QList<QTextEdit::ExtraSelection> &p_selections) Q_DECL_OVERRIDE
@@ -165,6 +169,9 @@ signals:
     // Will be emitted by VImagePreviewer for now.
     void statusChanged();
 
+    // Request to convert @p_text to Html.
+    void requestTextToHtml(const QString &p_text);
+
 protected:
     void updateFontAndPalette() Q_DECL_OVERRIDE;
 
@@ -191,6 +198,9 @@ private slots:
     // When there is no header in current cursor, will signal an invalid header.
     void updateCurrentHeader();
 
+    // Copy selected text as HTML.
+    void handleCopyAsHtmlAction();
+
 private:
     // Update the config of VTextEdit according to global configurations.
     void updateTextEditConfig();
@@ -222,5 +232,7 @@ private:
     QVector<VTableOfContentItem> m_headers;
 
     bool m_freshEdit;
+
+    VCopyTextAsHtmlDialog *m_textToHtmlDialog;
 };
 #endif // VMDEDITOR_H

+ 24 - 0
src/vmdtab.cpp

@@ -413,6 +413,11 @@ void VMdTab::setupMarkdownViewer()
 
                 tabIsReady(TabReady::ReadMode);
             });
+    connect(m_document, &VDocument::textToHtmlFinished,
+            this, [this](const QString &p_text, const QString &p_html) {
+                Q_ASSERT(m_editor);
+                m_editor->textToHtmlFinished(p_text, m_webViewer->url(), p_html);
+            });
 
     page->setWebChannel(channel);
 
@@ -464,6 +469,8 @@ void VMdTab::setupMarkdownEditor()
 
                 tabIsReady(TabReady::EditMode);
             });
+    connect(m_editor, &VMdEditor::requestTextToHtml,
+            this, &VMdTab::textToHtmlViaWebView);
 
     enableHeadingSequence(m_enableHeadingSequence);
     m_editor->reloadFile();
@@ -1006,3 +1013,20 @@ void VMdTab::handleFileOrDirectoryChange(bool p_isFile, UpdateAction p_act)
         m_editor->refreshPreview();
     }
 }
+
+void VMdTab::textToHtmlViaWebView(const QString &p_text)
+{
+    int maxRetry = 50;
+    while (!m_document->isReadyToTextToHtml() && maxRetry > 0) {
+        qDebug() << "wait for web side ready to convert text to HTML";
+        VUtils::sleepWait(100);
+        --maxRetry;
+    }
+
+    if (maxRetry == 0) {
+        qWarning() << "web side is not ready to convert text to HTML";
+        return;
+    }
+
+    m_document->textToHtmlAsync(p_text);
+}

+ 2 - 0
src/vmdtab.h

@@ -184,6 +184,8 @@ private:
     // updateStatus() with only cursor position information.
     void updateCursorStatus();
 
+    void textToHtmlViaWebView(const QString &p_text);
+
     VMdEditor *m_editor;
     VWebView *m_webViewer;
     VDocument *m_document;

+ 33 - 0
src/vwaitingwidget.cpp

@@ -0,0 +1,33 @@
+#include "vwaitingwidget.h"
+
+#include <QLabel>
+#include <QPixmap>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+
+VWaitingWidget::VWaitingWidget(QWidget *p_parent)
+    : QWidget(p_parent)
+{
+    setupUI();
+}
+
+void VWaitingWidget::setupUI()
+{
+    QSize imgSize(64, 64);
+    QLabel *logoLabel = new QLabel();
+    logoLabel->setPixmap(QPixmap(":/resources/icons/vnote.svg").scaled(imgSize, Qt::KeepAspectRatio));
+
+    QHBoxLayout *layout = new QHBoxLayout();
+    layout->addStretch();
+    layout->addWidget(logoLabel);
+    layout->addStretch();
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addStretch();
+    mainLayout->addLayout(layout);
+    mainLayout->addStretch();
+
+    mainLayout->setContentsMargins(0, 0, 0, 0);
+
+    setLayout(mainLayout);
+}

+ 19 - 0
src/vwaitingwidget.h

@@ -0,0 +1,19 @@
+#ifndef VWAITINGWIDGET_H
+#define VWAITINGWIDGET_H
+
+#include <QWidget>
+
+class QLabel;
+
+
+class VWaitingWidget : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit VWaitingWidget(QWidget *p_parent = nullptr);
+
+private:
+    void setupUI();
+};
+
+#endif // VWAITINGWIDGET_H

+ 47 - 54
src/vwebview.cpp

@@ -10,12 +10,12 @@
 #include <QMimeData>
 #include <QApplication>
 #include <QImage>
-#include <QRegExp>
 #include <QFileInfo>
 #include "vfile.h"
 #include "utils/vclipboardutils.h"
 #include "utils/viconutils.h"
 #include "vconfigmanager.h"
+#include "utils/vwebutils.h"
 
 extern VConfigManager *g_config;
 
@@ -231,12 +231,50 @@ void VWebView::hideUnusedActions(QMenu *p_menu)
 
 static bool removeBackgroundColor(QString &p_html)
 {
-    QRegExp reg("(\\s|\")background(-color)?:[^;]+;");
+    QRegExp reg("(<[^>]+\\sstyle=[^>]*(\\s|\"))background(-color)?:[^;]+;([^>]*>)");
     int size = p_html.size();
-    p_html.replace(reg, "\\1");
+    p_html.replace(reg, "\\1\\4");
     return p_html.size() != size;
 }
 
+bool VWebView::removeStyles(QString &p_html)
+{
+    bool changed = false;
+
+    const QStringList &styles = g_config->getStylesToRemoveWhenCopied();
+    if (styles.isEmpty()) {
+        return changed;
+    }
+
+    QRegExp tagReg("(<[^>]+\\sstyle=[^>]*>)");
+
+    int pos = 0;
+    while (pos < p_html.size()) {
+        int idx = p_html.indexOf(tagReg, pos);
+        if (idx == -1) {
+            break;
+        }
+
+        QString styleStr = tagReg.cap(1);
+        QString alteredStyleStr = styleStr;
+
+        QString regPatt("(\\s|\")%1:[^;]+;");
+        for (auto const & sty : styles) {
+            QRegExp reg(regPatt.arg(sty));
+            alteredStyleStr.replace(reg, "\\1");
+        }
+
+        pos = idx + tagReg.matchedLength();
+        if (styleStr != alteredStyleStr) {
+            pos = pos + alteredStyleStr.size() - styleStr.size();
+            p_html.replace(idx, tagReg.matchedLength(), alteredStyleStr);
+            changed = true;
+        }
+    }
+
+    return changed;
+}
+
 void VWebView::handleCopyWithoutBackgroundAction()
 {
     m_needRemoveBackground = true;
@@ -280,56 +318,6 @@ void VWebView::handleClipboardChanged(QClipboard::Mode p_mode)
     }
 }
 
-bool VWebView::fixImgSrc(QString &p_html)
-{
-    bool changed = false;
-
-#if defined(Q_OS_WIN)
-    QUrl::ComponentFormattingOption strOpt = QUrl::EncodeSpaces;
-#else
-    QUrl::ComponentFormattingOption strOpt = QUrl::FullyEncoded;
-#endif
-
-    QRegExp reg("(<img src=\")([^\"]+)\"");
-    QUrl baseUrl(url());
-
-    int pos = 0;
-    while (pos < p_html.size()) {
-        int idx = p_html.indexOf(reg, pos);
-        if (idx == -1) {
-            break;
-        }
-
-        QString urlStr = reg.cap(2);
-        QUrl imgUrl(urlStr);
-
-        QString fixedStr;
-        if (imgUrl.isRelative()) {
-            fixedStr = baseUrl.resolved(imgUrl).toString(strOpt);
-        } else if (imgUrl.isLocalFile()) {
-            fixedStr = imgUrl.toString(strOpt);
-        } else if (imgUrl.scheme() != "https" && imgUrl.scheme() != "http") {
-            QString tmp = imgUrl.toString();
-            if (QFileInfo::exists(tmp)) {
-                fixedStr = QUrl::fromLocalFile(tmp).toString(strOpt);
-            }
-        }
-
-        pos = idx + reg.matchedLength();
-        if (!fixedStr.isEmpty() && urlStr != fixedStr) {
-            qDebug() << "fix img url" << urlStr << fixedStr;
-            // Insert one more space to avoid fix the url twice.
-            pos = pos + fixedStr.size() + 1 - urlStr.size();
-            p_html.replace(idx,
-                           reg.matchedLength(),
-                           QString("<img  src=\"%1\"").arg(fixedStr));
-            changed = true;
-        }
-    }
-
-    return changed;
-}
-
 void VWebView::alterHtmlMimeData(QClipboard *p_clipboard,
                                  const QMimeData *p_mimeData,
                                  bool p_removeBackground)
@@ -353,7 +341,12 @@ void VWebView::alterHtmlMimeData(QClipboard *p_clipboard,
     }
 
     // Fix local relative images.
-    if (m_fixImgSrc && fixImgSrc(html)) {
+    if (m_fixImgSrc && VWebUtils::fixImageSrcInHtml(url(), html)) {
+        altered = true;
+    }
+
+    // Fix margin and padding.
+    if (removeStyles(html)) {
         altered = true;
     }
 

+ 3 - 2
src/vwebview.h

@@ -3,6 +3,7 @@
 
 #include <QWebEngineView>
 #include <QClipboard>
+#include <QRegExp>
 
 class VFile;
 class QMenu;
@@ -42,11 +43,11 @@ private:
                            const QMimeData *p_mimeData,
                            bool p_removeBackground);
 
-    bool fixImgSrc(QString &p_html);
-
     void removeHtmlFromImageData(QClipboard *p_clipboard,
                                  const QMimeData *p_mimeData);
 
+    bool removeStyles(QString &p_html);
+
     VFile *m_file;
 
     // Whether this view has hooked the Copy Image Url action.