From 14df9e6e90927a3d2444477088e24c9429695948 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Tue, 9 Jan 2018 20:20:32 +0800 Subject: [PATCH] refine copy HTML logics --- src/dialog/vcopytextashtmldialog.cpp | 21 +- src/dialog/vcopytextashtmldialog.h | 6 +- src/resources/vnote.ini | 26 +- src/utils/vwebutils.cpp | 503 ++++++++++++++++++++++++++- src/utils/vwebutils.h | 108 +++++- src/vconfigmanager.cpp | 6 - src/vconfigmanager.h | 36 +- src/vmainwindow.cpp | 10 +- src/vmainwindow.h | 3 + src/vmdeditor.cpp | 42 ++- src/vmdeditor.h | 4 +- src/vwebview.cpp | 160 +++++---- src/vwebview.h | 17 +- 13 files changed, 789 insertions(+), 153 deletions(-) diff --git a/src/dialog/vcopytextashtmldialog.cpp b/src/dialog/vcopytextashtmldialog.cpp index 5ba12fec..46ab0026 100644 --- a/src/dialog/vcopytextashtmldialog.cpp +++ b/src/dialog/vcopytextashtmldialog.cpp @@ -13,8 +13,12 @@ extern VConfigManager *g_config; -VCopyTextAsHtmlDialog::VCopyTextAsHtmlDialog(const QString &p_text, QWidget *p_parent) - : QDialog(p_parent), m_text(p_text) +extern VWebUtils *g_webUtils; + +VCopyTextAsHtmlDialog::VCopyTextAsHtmlDialog(const QString &p_text, + const QString &p_copyTarget, + QWidget *p_parent) + : QDialog(p_parent), m_text(p_text), m_copyTarget(p_copyTarget) { setupUI(); } @@ -47,7 +51,7 @@ void VCopyTextAsHtmlDialog::setupUI() mainLayout->addWidget(m_btnBox); setLayout(mainLayout); - setWindowTitle(tr("Copy Text As HTML")); + setWindowTitle(tr("Copy Text As HTML (%1)").arg(m_copyTarget)); setHtmlVisible(false); } @@ -61,16 +65,11 @@ void VCopyTextAsHtmlDialog::setHtmlVisible(bool p_visible) void VCopyTextAsHtmlDialog::setConvertedHtml(const QUrl &p_baseUrl, const QString &p_html) { - QString html = QString("%1").arg(p_html); - m_htmlViewer->setHtml(html, p_baseUrl); + QString html = p_html; + m_htmlViewer->setHtml("" + html + "", p_baseUrl); setHtmlVisible(true); - VWebUtils::translateColors(html); - - // Fix image source. - if (g_config->getFixImageSrcInWebWhenCopied()) { - VWebUtils::fixImageSrcInHtml(p_baseUrl, html); - } + g_webUtils->alterHtmlAsTarget(p_baseUrl, html, m_copyTarget); QClipboard *clipboard = QApplication::clipboard(); QMimeData *data = new QMimeData(); diff --git a/src/dialog/vcopytextashtmldialog.h b/src/dialog/vcopytextashtmldialog.h index 93d935b1..a4bad341 100644 --- a/src/dialog/vcopytextashtmldialog.h +++ b/src/dialog/vcopytextashtmldialog.h @@ -15,7 +15,9 @@ class VCopyTextAsHtmlDialog : public QDialog { Q_OBJECT public: - VCopyTextAsHtmlDialog(const QString &p_text, QWidget *p_parent = nullptr); + VCopyTextAsHtmlDialog(const QString &p_text, + const QString &p_copyTarget, + QWidget *p_parent = nullptr); void setConvertedHtml(const QUrl &p_baseUrl, const QString &p_html); @@ -37,6 +39,8 @@ private: QDialogButtonBox *m_btnBox; QString m_text; + + QString m_copyTarget; }; inline const QString &VCopyTextAsHtmlDialog::getText() const diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index 05f32c98..0279e6ed 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -193,17 +193,31 @@ custom_colors=White:#FFFFFF,LightGrey:#EEEEEE ; Location and configuration for Mathjax mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML -; Fix local relative image source when copied -fix_img_src_when_copied=true - -; Styles to be removed when copied in read mode +; Styles to be removed when copied ; style1,style2,style3 -styles_to_remove_when_copied=margin,margin-left,margin-right,padding,padding-left,padding-right +styles_to_remove_when_copied= + +; Styles when transform to +style_of_span_for_mark="background-color: #FFFF00;" ; 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 +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:background-color,code$font-family:font-size:line-height:color:display:overfow-x:background-color,li$line-height:background-color,a$color:vertical-align:background-color,pre$display:overflow-y:overflow-x:color:font-size:font-style:font-weight:letter-spacing:text-align:text-indent:word-spacing:background-color + +; Define targets the copied content will be pasted into +; target_name$action1:action2:action3,targeet_name2$action2:action3 +; Available actions: +; s - add surrounding tags +; b(tag1|tag2) - remove background color of all tags except tag1 and tag2 +; c(tag1|tag2) - translate colors using palette defined mapping except tag1 and tag2 +; i - fix local relative +; m(tag1|tag2) - remove margin/margin-left/margin-right/padding/padding-left/padding-right of all tags except tag1 and tag2 +; r - raw html with all styles removed +; a - transform to +; x(tag1|tag2) - remove styles specified in [styles_to_inline_when_copied] of all tags except tag1 and tag2 +; p - replace the background color of
 with that of its child 
+copy_targets=WithoutBackground$s:b(mark):c:i:x,OneNote$s:b(mark):c:i:m:a:x,MicroSoftWord$s:p:b(mark|pre):c(pre):i:m:a:x,RawHTML$r
 
 [shortcuts]
 ; Define shortcuts here, with each item in the form "operation=keysequence".
diff --git a/src/utils/vwebutils.cpp b/src/utils/vwebutils.cpp
index 98d14c44..2cdeb335 100644
--- a/src/utils/vwebutils.cpp
+++ b/src/utils/vwebutils.cpp
@@ -5,14 +5,79 @@
 #include 
 
 #include "vpalette.h"
+#include "vconfigmanager.h"
 
 extern VPalette *g_palette;
 
+extern VConfigManager *g_config;
+
 VWebUtils::VWebUtils()
 {
 }
 
-bool VWebUtils::fixImageSrcInHtml(const QUrl &p_baseUrl, QString &p_html)
+void VWebUtils::init()
+{
+    m_stylesToRemoveWhenCopied = g_config->getStylesToRemoveWhenCopied();
+
+    m_styleOfSpanForMark = g_config->getStyleOfSpanForMark();
+
+    m_tagReg = QRegExp("<([^>/\\s]+)([^>]*)>");
+
+    m_styleTagReg = QRegExp("<([^>\\s]+)([^>]*\\s)style=\"([^\">]+)\"([^>]*)>");
+
+    initCopyTargets(g_config->getCopyTargets());
+}
+
+void VWebUtils::initCopyTargets(const QStringList &p_str)
+{
+    Q_ASSERT(m_copyTargets.isEmpty());
+    // cap(1): action;
+    // cap(3): arguments;
+    QRegExp actReg("([0-9a-zA-Z])(\\(([^\\)]*)\\))?");
+
+    for (auto const & str : p_str) {
+        auto vals = str.split('$');
+        if (vals.size() != 2) {
+            continue;
+        }
+
+        CopyTarget tar;
+        tar.m_name = vals[0];
+        if (tar.m_name.isEmpty()) {
+            continue;
+        }
+
+        auto acts = vals[1].split(':');
+        for (auto const & it : acts) {
+            if (it.isEmpty()) {
+                continue;
+            }
+
+            if (!actReg.exactMatch(it)) {
+                continue;
+            }
+
+            if (actReg.cap(1).size() != 1) {
+                continue;
+            }
+
+            CopyTargetAction act;
+            act.m_act = actReg.cap(1)[0];
+
+            if (!actReg.cap(3).isEmpty()) {
+                act.m_args = actReg.cap(3).toLower().split('|');
+            }
+
+            tar.m_actions.append(act);
+        }
+
+        m_copyTargets.append(tar);
+    }
+
+    qDebug() << "init" << m_copyTargets.size() << "copy targets";
+}
+
+bool VWebUtils::fixImageSrc(const QUrl &p_baseUrl, QString &p_html)
 {
     bool changed = false;
 
@@ -61,15 +126,142 @@ bool VWebUtils::fixImageSrcInHtml(const QUrl &p_baseUrl, QString &p_html)
     return changed;
 }
 
-bool VWebUtils::removeBackgroundColor(QString &p_html)
+QStringList VWebUtils::getCopyTargetsName() const
 {
-    QRegExp reg("(<[^>]+\\sstyle=[^>]*(\\s|\"))background(-color)?:[^;]+;([^>]*>)");
-    int size = p_html.size();
-    p_html.replace(reg, "\\1\\4");
-    return p_html.size() != size;
+    QStringList names;
+    for (auto const & it : m_copyTargets) {
+        names << it.m_name;
+    }
+
+    return names;
 }
 
-bool VWebUtils::translateColors(QString &p_html)
+bool VWebUtils::alterHtmlAsTarget(const QUrl &p_baseUrl, QString &p_html, const QString &p_target) const
+{
+    int idx = targetIndex(p_target);
+    if (idx == -1) {
+        return false;
+    }
+
+    bool altered = false;
+    for (auto const & act : m_copyTargets[idx].m_actions) {
+        if (const_cast(this)->alterHtmlByTargetAction(p_baseUrl, p_html, act)) {
+            altered = true;
+        }
+    }
+
+    return altered;
+}
+
+int VWebUtils::targetIndex(const QString &p_target) const
+{
+    if (p_target.isEmpty()) {
+        return -1;
+    }
+
+    for (int i = 0; i < m_copyTargets.size(); ++i) {
+        if (m_copyTargets[i].m_name == p_target) {
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+bool VWebUtils::alterHtmlByTargetAction(const QUrl &p_baseUrl, QString &p_html, const CopyTargetAction &p_action)
+{
+    bool altered = false;
+    switch (p_action.m_act.toLatin1()) {
+    case 's':
+        if (!p_html.startsWith("")) {
+            p_html = "" + p_html + "";
+            altered = true;
+        }
+
+        break;
+
+    case 'b':
+        altered = removeBackgroundColor(p_html, p_action.m_args);
+        break;
+
+    case 'c':
+        altered = translateColors(p_html, p_action.m_args);
+        break;
+
+    case 'i':
+        altered = fixImageSrc(p_baseUrl, p_html);
+        break;
+
+    case 'm':
+        altered = removeMarginPadding(p_html, p_action.m_args);
+        break;
+
+    case 'x':
+        altered = removeStylesToRemoveWhenCopied(p_html, p_action.m_args);
+        break;
+
+    case 'r':
+        altered = removeAllStyles(p_html, p_action.m_args);
+        break;
+
+    case 'a':
+        altered = transformMarkToSpan(p_html);
+        break;
+
+    case 'p':
+        altered = replacePreBackgroundColorWithCode(p_html);
+        break;
+
+    default:
+        break;
+    }
+
+    return altered;
+}
+
+static int skipToTagEnd(const QString &p_html, int p_pos, const QString &p_tag)
+{
+    QRegExp beginReg(QString("<%1 ").arg(p_tag));
+    QRegExp endReg(QString("").arg(p_tag));
+
+    int pos = p_pos;
+    int nBegin = p_html.indexOf(beginReg, pos);
+    int nEnd = p_html.indexOf(endReg, pos);
+    if (nBegin > -1 && nBegin < nEnd) {
+        // Nested tag.
+        pos = skipToTagEnd(p_html, nBegin + beginReg.matchedLength(), p_tag);
+        nEnd = p_html.indexOf(endReg, pos);
+    }
+
+    if (nEnd > -1) {
+        pos = nEnd + endReg.matchedLength();
+    }
+
+    return pos;
+}
+
+// @p_html is the style string.
+static bool removeStylesInStyleString(QString &p_html, const QStringList &p_styles)
+{
+    if (p_styles.isEmpty()) {
+        return false;
+    }
+
+    int size = p_html.size();
+    QRegExp reg(QString("(\\s|^)(%1):[^:]+;").arg(p_styles.join('|')));
+    p_html.remove(reg);
+
+    return size != p_html.size();
+}
+
+bool VWebUtils::removeBackgroundColor(QString &p_html, const QStringList &p_skipTags)
+{
+    QStringList styles({"background", "background-color"});
+
+    return removeStyles(p_html, p_skipTags, styles);
+}
+
+bool VWebUtils::translateColors(QString &p_html, const QStringList &p_skipTags)
 {
     bool changed = false;
 
@@ -78,20 +270,34 @@ bool VWebUtils::translateColors(QString &p_html)
         return changed;
     }
 
-    QRegExp tagReg("(<[^>]+\\sstyle=[^>]*>)");
     // Won't mixed up with background-color.
-    QRegExp colorReg("(\\s|\")color:([^;]+);");
+    QRegExp colorReg("(\\s|^)color:([^;]+);");
 
     int pos = 0;
     while (pos < p_html.size()) {
-        int idx = p_html.indexOf(tagReg, pos);
-        if (idx == -1) {
+        int tagIdx = p_html.indexOf(m_tagReg, pos);
+        if (tagIdx == -1) {
             break;
         }
 
-        QString styleStr = tagReg.cap(1);
-        QString alteredStyleStr = styleStr;
+        QString tagName = m_tagReg.cap(1);
+        if (p_skipTags.contains(tagName.toLower())) {
+            // Skip this tag.
+            pos = skipToTagEnd(p_html, tagIdx + m_tagReg.matchedLength(), tagName);
+            continue;
+        }
 
+        pos = tagIdx;
+        int idx = p_html.indexOf(m_styleTagReg, pos);
+        if (idx == -1) {
+            break;
+        } else if (idx != tagIdx) {
+            pos = tagIdx + m_tagReg.matchedLength();
+            continue;
+        }
+
+        QString styleStr = m_styleTagReg.cap(3);
+        QString alteredStyleStr = styleStr;
         int posb = 0;
         while (posb < alteredStyleStr.size()) {
             int idxb = alteredStyleStr.indexOf(colorReg, posb);
@@ -108,19 +314,280 @@ bool VWebUtils::translateColors(QString &p_html)
 
             // Replace the color.
             QString newCol = it.value();
-            // Add one extra space between color and :.
-            QString newStr = QString("%1color : %2;").arg(colorReg.cap(1)).arg(newCol);
+            // Should not add extra space before :.
+            QString newStr = QString("%1color: %2;").arg(colorReg.cap(1)).arg(newCol);
             alteredStyleStr.replace(idxb, colorReg.matchedLength(), newStr);
             posb = idxb + newStr.size();
             changed = true;
         }
 
-        pos = idx + tagReg.matchedLength();
         if (changed) {
-            pos = pos + alteredStyleStr.size() - styleStr.size();
-            p_html.replace(idx, tagReg.matchedLength(), alteredStyleStr);
+            QString newTag = QString("<%1%2style=\"%3\"%4>").arg(m_styleTagReg.cap(1))
+                                                            .arg(m_styleTagReg.cap(2))
+                                                            .arg(alteredStyleStr)
+                                                            .arg(m_styleTagReg.cap(4));
+
+            p_html.replace(idx, m_styleTagReg.matchedLength(), newTag);
+
+            pos = idx + newTag.size();
+        } else {
+            pos = idx + m_styleTagReg.matchedLength();
         }
     }
 
     return changed;
 }
+
+bool VWebUtils::removeMarginPadding(QString &p_html, const QStringList &p_skipTags)
+{
+    QStringList styles({"margin", "margin-left", "margin-right",
+                        "padding", "padding-left", "padding-right"});
+
+    return removeStyles(p_html, p_skipTags, styles);
+}
+
+bool VWebUtils::removeStyles(QString &p_html, const QStringList &p_skipTags, const QStringList &p_styles)
+{
+    if (p_styles.isEmpty()) {
+        return false;
+    }
+
+    bool altered = false;
+    int pos = 0;
+
+    while (pos < p_html.size()) {
+        int tagIdx = p_html.indexOf(m_tagReg, pos);
+        if (tagIdx == -1) {
+            break;
+        }
+
+        QString tagName = m_tagReg.cap(1);
+        if (p_skipTags.contains(tagName.toLower())) {
+            // Skip this tag.
+            pos = skipToTagEnd(p_html, tagIdx + m_tagReg.matchedLength(), tagName);
+            continue;
+        }
+
+        pos = tagIdx;
+        int idx = p_html.indexOf(m_styleTagReg, pos);
+        if (idx == -1) {
+            break;
+        } else if (idx != tagIdx) {
+            pos = tagIdx + m_tagReg.matchedLength();
+            continue;
+        }
+
+        QString styleStr = m_styleTagReg.cap(3);
+        if (removeStylesInStyleString(styleStr, p_styles)) {
+            QString newTag = QString("<%1%2style=\"%3\"%4>").arg(m_styleTagReg.cap(1))
+                                                            .arg(m_styleTagReg.cap(2))
+                                                            .arg(styleStr)
+                                                            .arg(m_styleTagReg.cap(4));
+            p_html.replace(idx, m_styleTagReg.matchedLength(), newTag);
+
+            pos = idx + newTag.size();
+
+            altered = true;
+        } else {
+            pos = idx + m_styleTagReg.matchedLength();
+        }
+    }
+
+    return altered;
+}
+
+bool VWebUtils::removeStylesToRemoveWhenCopied(QString &p_html, const QStringList &p_skipTags)
+{
+    return removeStyles(p_html, p_skipTags, m_stylesToRemoveWhenCopied);
+}
+
+bool VWebUtils::removeAllStyles(QString &p_html, const QStringList &p_skipTags)
+{
+    bool altered = false;
+    int pos = 0;
+
+    while (pos < p_html.size()) {
+        int tagIdx = p_html.indexOf(m_tagReg, pos);
+        if (tagIdx == -1) {
+            break;
+        }
+
+        QString tagName = m_tagReg.cap(1);
+        if (p_skipTags.contains(tagName.toLower())) {
+            // Skip this tag.
+            pos = skipToTagEnd(p_html, tagIdx + m_tagReg.matchedLength(), tagName);
+            continue;
+        }
+
+        pos = tagIdx;
+        int idx = p_html.indexOf(m_styleTagReg, pos);
+        if (idx == -1) {
+            break;
+        } else if (idx != tagIdx) {
+            pos = tagIdx + m_tagReg.matchedLength();
+            continue;
+        }
+
+        QString newTag = QString("<%1%2%3>").arg(m_styleTagReg.cap(1))
+                                            .arg(m_styleTagReg.cap(2))
+                                            .arg(m_styleTagReg.cap(4));
+        p_html.replace(idx, m_styleTagReg.matchedLength(), newTag);
+
+        pos = idx + newTag.size();
+
+        altered = true;
+    }
+
+    return altered;
+}
+
+bool VWebUtils::transformMarkToSpan(QString &p_html)
+{
+    bool altered = false;
+    int pos = 0;
+
+    while (pos < p_html.size()) {
+        int tagIdx = p_html.indexOf(m_tagReg, pos);
+        if (tagIdx == -1) {
+            break;
+        }
+
+        QString tagName = m_tagReg.cap(1);
+        if (tagName.toLower() != "mark") {
+            pos = tagIdx + m_tagReg.matchedLength();
+            continue;
+        }
+
+        pos = tagIdx;
+        int idx = p_html.indexOf(m_styleTagReg, pos);
+        if (idx == -1 || idx != tagIdx) {
+            //  without "style".
+            QString newTag = QString("").arg(m_styleOfSpanForMark)
+                                                              .arg(m_tagReg.cap(2));
+            p_html.replace(tagIdx, m_tagReg.matchedLength(), newTag);
+
+            pos = tagIdx + newTag.size();
+
+            altered = true;
+            continue;
+        }
+
+        QString newTag = QString("").arg(m_styleTagReg.cap(2))
+                                                          .arg(m_styleTagReg.cap(3) + m_styleOfSpanForMark)
+                                                          .arg(m_styleTagReg.cap(4));
+        p_html.replace(idx, m_styleTagReg.matchedLength(), newTag);
+
+        pos = idx + newTag.size();
+
+        altered = true;
+    }
+
+    if (altered) {
+        // Replace all  with .
+        p_html.replace("", "");
+    }
+
+    return altered;
+}
+
+bool VWebUtils::replacePreBackgroundColorWithCode(QString &p_html)
+{
+    if (p_html.isEmpty()) {
+        return false;
+    }
+
+    bool altered = false;
+    int pos = 0;
+
+    QRegExp bgReg("(\\s|^)(background(-color)?:[^;]+;)");
+
+    while (pos < p_html.size()) {
+        int tagIdx = p_html.indexOf(m_tagReg, pos);
+        if (tagIdx == -1) {
+            break;
+        }
+
+        QString tagName = m_tagReg.cap(1);
+        pos = tagIdx + m_tagReg.matchedLength();
+        if (tagName.toLower() != "pre") {
+            continue;
+        }
+
+        int preEnd = skipToTagEnd(p_html, pos, tagName);
+
+        HtmlTag nextTag = readNextTag(p_html, pos);
+        if (nextTag.m_name != "code"
+            || nextTag.m_start >= preEnd
+            || nextTag.m_style.isEmpty()) {
+            continue;
+        }
+
+        // Get the background style of .
+        int idx = nextTag.m_style.indexOf(bgReg);
+        if (idx == -1) {
+            continue;
+        }
+
+        QString bgStyle = bgReg.cap(2);
+
+        pos = tagIdx;
+        idx = p_html.indexOf(m_styleTagReg, pos);
+        if (idx == -1 || idx != tagIdx) {
+            // 
 without "style".
+            QString newTag = QString("<%1 style=\"%2\" %3>").arg(m_tagReg.cap(1))
+                                                            .arg(bgStyle)
+                                                            .arg(m_tagReg.cap(2));
+            p_html.replace(tagIdx, m_tagReg.matchedLength(), newTag);
+
+            pos = tagIdx + newTag.size();
+
+            altered = true;
+            continue;
+        }
+
+        QString newTag;
+        if (m_styleTagReg.cap(3).indexOf(bgReg) == -1) {
+            // No background style specified.
+            newTag = QString("<%1%2style=\"%3\"%4>").arg(m_styleTagReg.cap(1))
+                                                    .arg(m_styleTagReg.cap(2))
+                                                    .arg(m_styleTagReg.cap(3) + bgStyle)
+                                                    .arg(m_styleTagReg.cap(4));
+        } else {
+            // Replace background style.
+            newTag = QString("<%1%2style=\"%3\"%4>").arg(m_styleTagReg.cap(1))
+                                                    .arg(m_styleTagReg.cap(2))
+                                                    .arg(m_styleTagReg.cap(3).replace(bgReg, " " + bgStyle))
+                                                    .arg(m_styleTagReg.cap(4));
+        }
+
+        p_html.replace(idx, m_styleTagReg.matchedLength(), newTag);
+
+        pos = idx + newTag.size();
+
+        altered = true;
+    }
+
+    return altered;
+}
+
+VWebUtils::HtmlTag VWebUtils::readNextTag(const QString &p_html, int p_pos)
+{
+    HtmlTag tag;
+
+    int tagIdx = p_html.indexOf(m_tagReg, p_pos);
+    if (tagIdx == -1) {
+        return tag;
+    }
+
+    tag.m_name = m_tagReg.cap(1);
+    tag.m_start = tagIdx;
+    tag.m_end = skipToTagEnd(p_html, tagIdx + m_tagReg.matchedLength(), tag.m_name);
+
+    int idx = p_html.indexOf(m_styleTagReg, tagIdx);
+    if (idx == -1 || idx != tagIdx) {
+        return tag;
+    }
+
+    tag.m_style = m_styleTagReg.cap(3);
+    return tag;
+}
diff --git a/src/utils/vwebutils.h b/src/utils/vwebutils.h
index 289e7680..e66c89a1 100644
--- a/src/utils/vwebutils.h
+++ b/src/utils/vwebutils.h
@@ -3,22 +3,114 @@
 
 #include 
 #include 
+#include 
+#include 
+#include 
 
 
 class VWebUtils
 {
 public:
-    // Fix  in @p_html.
-    static bool fixImageSrcInHtml(const QUrl &p_baseUrl, QString &p_html);
+    VWebUtils();
 
-    // Remove background color style in @p_html.
-    static bool removeBackgroundColor(QString &p_html);
+    void init();
 
-    // Translate color styles in @p_html using mappings from VPalette.
-    static bool translateColors(QString &p_html);
+    QStringList getCopyTargetsName() const;
+
+    // Alter @p_html using @p_target.
+    // Returns true if @p_html is modified.
+    bool alterHtmlAsTarget(const QUrl &p_baseUrl, QString &p_html, const QString &p_target) const;
 
 private:
-    VWebUtils();
-};
+    struct CopyTargetAction
+    {
+        QChar m_act;
 
+        QStringList m_args;
+    };
+
+    struct CopyTarget
+    {
+        QString m_name;
+
+        QVector m_actions;
+    };
+
+    struct HtmlTag
+    {
+        HtmlTag()
+            : m_start(-1), m_end(-1)
+        {
+
+        }
+
+        bool isNull()
+        {
+            return m_name.isEmpty();
+        }
+
+        QString m_name;
+        QString m_style;
+
+        int m_start;
+        int m_end;
+    };
+
+    void initCopyTargets(const QStringList &p_str);
+
+    // Return the index in m_copyTargets of @p_target.
+    int targetIndex(const QString &p_target) const;
+
+    bool alterHtmlByTargetAction(const QUrl &p_baseUrl, QString &p_html, const CopyTargetAction &p_action);
+
+    // Remove background color style in @p_html of all tags except @p_skipTags.
+    bool removeBackgroundColor(QString &p_html, const QStringList &p_skipTags);
+
+    // Translate color styles in @p_html using mappings from VPalette.
+    bool translateColors(QString &p_html, const QStringList &p_skipTags);
+
+    // Fix  in @p_html.
+    bool fixImageSrc(const QUrl &p_baseUrl, QString &p_html);
+
+    // Remove margin/padding/margin-left/right/padding-left/right.
+    bool removeMarginPadding(QString &p_html, const QStringList &p_skipTags);
+
+    bool removeStyles(QString &p_html, const QStringList &p_skipTags, const QStringList &p_styles);
+
+    // Remove styles specified in [web]/styles_to_remove_when_copied.
+    bool removeStylesToRemoveWhenCopied(QString &p_html, const QStringList &p_skipTags);
+
+    // Remove all styles.
+    bool removeAllStyles(QString &p_html, const QStringList &p_skipTags);
+
+    // Transform  to .
+    bool transformMarkToSpan(QString &p_html);
+
+    // Replace the background color of 
 with that of its child .
+    bool replacePreBackgroundColorWithCode(QString &p_html);
+
+    VWebUtils::HtmlTag readNextTag(const QString &p_html, int p_pos);
+
+    QVector m_copyTargets;
+
+    // Custom styles to remove when copied.
+    QStringList m_stylesToRemoveWhenCopied;
+
+    // Style of  which is transformed from .
+    QString m_styleOfSpanForMark;
+
+    // Html start tag.
+    // Captured texts:
+    // 1. The tag name like 'code';
+    // 2. Text after tag name and before the end '>';
+    QRegExp m_tagReg;
+
+    // Html start tag with "style" defined.
+    // Captured texts:
+    // 1. The tag name like 'code';
+    // 2. Text before 'style=""';
+    // 3. Text inside 'style=""';
+    // 4. Text after 'style=""' and before '>';
+    QRegExp m_styleTagReg;
+};
 #endif // VWEBUTILS_H
diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp
index 0e6956c9..7dd44d84 100644
--- a/src/vconfigmanager.cpp
+++ b/src/vconfigmanager.cpp
@@ -285,12 +285,6 @@ void VConfigManager::initialize()
     m_closeBeforeExternalEditor = getConfigFromSettings("global",
                                                         "close_before_external_editor").toBool();
 
-    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(",");
 }
diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h
index cb1a7b80..40a3cb84 100644
--- a/src/vconfigmanager.h
+++ b/src/vconfigmanager.h
@@ -426,12 +426,15 @@ public:
 
     bool getCloseBeforeExternalEditor() const;
 
-    bool getFixImageSrcInWebWhenCopied() const;
-
-    const QStringList &getStylesToRemoveWhenCopied() const;
+    QStringList getStylesToRemoveWhenCopied() const;
 
     const QString &getStylesToInlineWhenCopied() const;
 
+    QString getStyleOfSpanForMark() const;
+
+    // Return [web]/copy_targets.
+    QStringList getCopyTargets() const;
+
 private:
     // Look up a config from user and default settings.
     QVariant getConfigFromSettings(const QString §ion, const QString &key) const;
@@ -822,12 +825,6 @@ private:
     // Whether user has reset the configurations.
     bool m_hasReset;
 
-    // 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;
 
@@ -1998,18 +1995,27 @@ inline bool VConfigManager::getCloseBeforeExternalEditor() const
     return m_closeBeforeExternalEditor;
 }
 
-inline bool VConfigManager::getFixImageSrcInWebWhenCopied() const
+inline QStringList VConfigManager::getStylesToRemoveWhenCopied() const
 {
-    return m_fixImageSrcInWebWhenCopied;
-}
+    return  getConfigFromSettings("web",
+                                  "styles_to_remove_when_copied").toStringList();
 
-inline const QStringList &VConfigManager::getStylesToRemoveWhenCopied() const
-{
-    return m_stylesToRemoveWhenCopied;
 }
 
 inline const QString &VConfigManager::getStylesToInlineWhenCopied() const
 {
     return m_stylesToInlineWhenCopied;
 }
+
+inline QStringList VConfigManager::getCopyTargets() const
+{
+    return getConfigFromSettings("web",
+                                 "copy_targets").toStringList();
+}
+
+inline QString VConfigManager::getStyleOfSpanForMark() const
+{
+    return getConfigFromSettings("web",
+                                 "style_of_span_for_mark").toString();
+}
 #endif // VCONFIGMANAGER_H
diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp
index eabd386e..158af2b3 100644
--- a/src/vmainwindow.cpp
+++ b/src/vmainwindow.cpp
@@ -36,14 +36,16 @@
 #include "utils/viconutils.h"
 #include "dialog/vtipsdialog.h"
 
-VMainWindow *g_mainWin;
-
 extern VConfigManager *g_config;
 
 extern VPalette *g_palette;
 
+VMainWindow *g_mainWin;
+
 VNote *g_vnote;
 
+VWebUtils *g_webUtils;
+
 const int VMainWindow::c_sharedMemTimerInterval = 1000;
 
 #if defined(QT_NO_DEBUG)
@@ -62,9 +64,13 @@ VMainWindow::VMainWindow(VSingleInstanceGuard *p_guard, QWidget *p_parent)
     g_mainWin = this;
 
     setWindowIcon(QIcon(":/resources/icons/vnote.ico"));
+
     vnote = new VNote(this);
     g_vnote = vnote;
 
+    m_webUtils.init();
+    g_webUtils = &m_webUtils;
+
     if (g_config->getEnableCompactMode()) {
         m_panelViewState = PanelViewState::CompactMode;
     } else {
diff --git a/src/vmainwindow.h b/src/vmainwindow.h
index 990a6c98..5a33cfb5 100644
--- a/src/vmainwindow.h
+++ b/src/vmainwindow.h
@@ -8,6 +8,7 @@
 #include 
 #include "vfile.h"
 #include "vedittab.h"
+#include "utils/vwebutils.h"
 
 class QLabel;
 class QComboBox;
@@ -376,6 +377,8 @@ private:
     // Whether user request VNote to quit.
     bool m_requestQuit;
 
+    VWebUtils m_webUtils;
+
     // Interval of the shared memory timer in ms.
     static const int c_sharedMemTimerInterval;
 };
diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp
index 338e6e51..318c6386 100644
--- a/src/vmdeditor.cpp
+++ b/src/vmdeditor.cpp
@@ -20,6 +20,9 @@
 #include "vpreviewmanager.h"
 #include "utils/viconutils.h"
 #include "dialog/vcopytextashtmldialog.h"
+#include "utils/vwebutils.h"
+
+extern VWebUtils *g_webUtils;
 
 extern VConfigManager *g_config;
 
@@ -275,12 +278,7 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
         const QList actions = menu->actions();
 
         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);
+            initCopyAsMenu(actions.isEmpty() ? NULL : actions.last(), menu);
         } else {
             QAction *saveExitAct = new QAction(VIconUtils::menuIcon(":/resources/icons/save_exit.svg"),
                                                tr("&Save Changes And Read"),
@@ -1042,7 +1040,7 @@ void VMdEditor::updateInitAndInsertedImages(bool p_fileChanged, UpdateAction p_a
     }
 }
 
-void VMdEditor::handleCopyAsHtmlAction()
+void VMdEditor::handleCopyAsAction(QAction *p_act)
 {
     QTextCursor cursor = textCursor();
     Q_ASSERT(cursor.hasSelection());
@@ -1051,7 +1049,7 @@ void VMdEditor::handleCopyAsHtmlAction()
     Q_ASSERT(!text.isEmpty());
 
     Q_ASSERT(!m_textToHtmlDialog);
-    m_textToHtmlDialog = new VCopyTextAsHtmlDialog(text, this);
+    m_textToHtmlDialog = new VCopyTextAsHtmlDialog(text, p_act->data().toString(), this);
 
     // For Hoedown, we use marked.js to convert the text to have a general interface.
     emit requestTextToHtml(text);
@@ -1129,3 +1127,31 @@ void VMdEditor::zoomPage(bool p_zoomIn, int p_range)
 
     m_mdHighlighter->rehighlight();
 }
+
+void VMdEditor::initCopyAsMenu(QAction *p_before, QMenu *p_menu)
+{
+    QStringList targets = g_webUtils->getCopyTargetsName();
+    if (targets.isEmpty()) {
+        return;
+    }
+
+    QMenu *subMenu = new QMenu(tr("Copy HTML As"), p_menu);
+    subMenu->setToolTipsVisible(true);
+    for (auto const & target : targets) {
+        QAction *act = new QAction(target, subMenu);
+        act->setData(target);
+        act->setToolTip(tr("Copy selected content as HTML using rules specified by target %1").arg(target));
+
+        subMenu->addAction(act);
+    }
+
+    connect(subMenu, &QMenu::triggered,
+            this, &VMdEditor::handleCopyAsAction);
+
+    QAction *menuAct = p_menu->insertMenu(p_before, subMenu);
+    if (p_before) {
+        p_menu->removeAction(p_before);
+        p_menu->insertAction(menuAct, p_before);
+        p_menu->insertSeparator(menuAct);
+    }
+}
diff --git a/src/vmdeditor.h b/src/vmdeditor.h
index 76428bc1..fbac42d2 100644
--- a/src/vmdeditor.h
+++ b/src/vmdeditor.h
@@ -211,7 +211,7 @@ private slots:
     void updateCurrentHeader();
 
     // Copy selected text as HTML.
-    void handleCopyAsHtmlAction();
+    void handleCopyAsAction(QAction *p_act);
 
 private:
     // Update the config of VTextEdit according to global configurations.
@@ -232,6 +232,8 @@ private:
     // We need to maintain the styles font size.
     void zoomPage(bool p_zoomIn, int p_range = 1);
 
+    void initCopyAsMenu(QAction *p_before, QMenu *p_menu);
+
     HGMarkdownHighlighter *m_mdHighlighter;
 
     VCodeBlockHighlightHelper *m_cbHighlighter;
diff --git a/src/vwebview.cpp b/src/vwebview.cpp
index f047be56..3be1df13 100644
--- a/src/vwebview.cpp
+++ b/src/vwebview.cpp
@@ -19,6 +19,8 @@
 
 extern VConfigManager *g_config;
 
+extern VWebUtils *g_webUtils;
+
 // We set the property of the clipboard to mark that the URL copied in the
 // clipboard has been altered.
 static const QString c_ClipboardPropertyMark = "CopiedImageURLAltered";
@@ -27,8 +29,6 @@ VWebView::VWebView(VFile *p_file, QWidget *p_parent)
     : QWebEngineView(p_parent),
       m_file(p_file),
       m_copyImageUrlActionHooked(false),
-      m_needRemoveBackground(false),
-      m_fixImgSrc(g_config->getFixImageSrcInWebWhenCopied()),
       m_afterCopyImage(false)
 {
     setAcceptDrops(false);
@@ -75,16 +75,10 @@ void VWebView::contextMenuEvent(QContextMenuEvent *p_event)
         }
     }
 
-    // Add Copy without Background action.
+    // Add Copy As menu.
     QAction *copyAct = pageAction(QWebEnginePage::Copy);
     if (actions.contains(copyAct)) {
-        QAction *copyWithoutBgAct = new QAction(tr("Copy &without Background"), menu);
-        copyWithoutBgAct->setToolTip(tr("Copy selected content without background styles"));
-        connect(copyWithoutBgAct, &QAction::triggered,
-                this, &VWebView::handleCopyWithoutBackgroundAction);
-        menu->insertAction(copyAct, copyWithoutBgAct);
-        menu->removeAction(copyAct);
-        menu->insertAction(copyWithoutBgAct, copyAct);
+        initCopyAsMenu(copyAct, menu);
     }
 
     // We need to replace the "Copy Image" action:
@@ -101,14 +95,8 @@ void VWebView::contextMenuEvent(QContextMenuEvent *p_event)
         defaultCopyImageAct->setVisible(false);
     }
 
-    // Add Copy All without Background action.
-    QAction *copyAllWithoutBgAct = new QAction(tr("Copy &All without Background"), menu);
-    copyAllWithoutBgAct->setToolTip(tr("Copy all contents without background styles"));
-    connect(copyAllWithoutBgAct, &QAction::triggered,
-            this, &VWebView::handleCopyAllWithoutBackgroundAction);
-    // Add it to the back.
-    menu->addSeparator();
-    menu->addAction(copyAllWithoutBgAct);
+    // Add Copy All As menu.
+    initCopyAllAsMenu(menu);
 
     hideUnusedActions(menu);
 
@@ -267,27 +255,10 @@ bool VWebView::removeStyles(QString &p_html)
     return changed;
 }
 
-void VWebView::handleCopyWithoutBackgroundAction()
-{
-    m_needRemoveBackground = true;
-
-    triggerPageAction(QWebEnginePage::Copy);
-}
-
-void VWebView::handleCopyAllWithoutBackgroundAction()
-{
-    triggerPageAction(QWebEnginePage::SelectAll);
-
-    m_needRemoveBackground = true;
-    triggerPageAction(QWebEnginePage::Copy);
-
-    triggerPageAction(QWebEnginePage::Unselect);
-}
-
 void VWebView::handleClipboardChanged(QClipboard::Mode p_mode)
 {
-    bool removeBackground = m_needRemoveBackground;
-    m_needRemoveBackground = false;
+    QString copyTarget = m_copyTarget;
+    m_copyTarget.clear();
 
     bool afterCopyImage = m_afterCopyImage;
     m_afterCopyImage = false;
@@ -306,49 +277,22 @@ void VWebView::handleClipboardChanged(QClipboard::Mode p_mode)
     if (afterCopyImage) {
         removeHtmlFromImageData(clipboard, mimeData);
     } else {
-        alterHtmlMimeData(clipboard, mimeData, removeBackground);
+        alterHtmlMimeData(clipboard, mimeData, copyTarget);
     }
 }
 
 void VWebView::alterHtmlMimeData(QClipboard *p_clipboard,
                                  const QMimeData *p_mimeData,
-                                 bool p_removeBackground)
+                                 const QString &p_copyTarget)
 {
-    if (!p_mimeData->hasHtml() || p_mimeData->hasImage()) {
+    if (!p_mimeData->hasHtml()
+        || p_mimeData->hasImage()
+        || p_copyTarget.isEmpty()) {
         return;
     }
 
-    bool altered = false;
     QString html = p_mimeData->html();
-
-    // Add surrounded tags.
-    if (!html.startsWith("")) {
-        altered = true;
-        html = QString("%1").arg(html);
-    }
-
-    // Remove background color.
-    if (p_removeBackground) {
-        if (VWebUtils::removeBackgroundColor(html)) {
-            altered = true;
-        }
-
-        if (VWebUtils::translateColors(html)) {
-            altered = true;
-        }
-    }
-
-    // Fix local relative images.
-    if (m_fixImgSrc && VWebUtils::fixImageSrcInHtml(url(), html)) {
-        altered = true;
-    }
-
-    // Fix margin and padding.
-    if (removeStyles(html)) {
-        altered = true;
-    }
-
-    if (!altered) {
+    if (!g_webUtils->alterHtmlAsTarget(url(), html, p_copyTarget)) {
         return;
     }
 
@@ -374,3 +318,79 @@ void VWebView::removeHtmlFromImageData(QClipboard *p_clipboard,
         VClipboardUtils::setMimeDataToClipboard(p_clipboard, data, QClipboard::Clipboard);
     }
 }
+
+void VWebView::initCopyAsMenu(QAction *p_after, QMenu *p_menu)
+{
+    QStringList targets = g_webUtils->getCopyTargetsName();
+    if (targets.isEmpty()) {
+        return;
+    }
+
+    QMenu *subMenu = new QMenu(tr("Copy As"), p_menu);
+    subMenu->setToolTipsVisible(true);
+    for (auto const & target : targets) {
+        QAction *act = new QAction(target, subMenu);
+        act->setData(target);
+        act->setToolTip(tr("Copy selected content using rules specified by target %1").arg(target));
+
+        subMenu->addAction(act);
+    }
+
+    connect(subMenu, &QMenu::triggered,
+            this, &VWebView::handleCopyAsAction);
+
+    QAction *menuAct = p_menu->insertMenu(p_after, subMenu);
+    p_menu->removeAction(p_after);
+    p_menu->insertAction(menuAct, p_after);
+}
+
+void VWebView::handleCopyAsAction(QAction *p_act)
+{
+    if (!p_act) {
+        return;
+    }
+
+    m_copyTarget = p_act->data().toString();
+
+    triggerPageAction(QWebEnginePage::Copy);
+}
+
+void VWebView::initCopyAllAsMenu(QMenu *p_menu)
+{
+    QStringList targets = g_webUtils->getCopyTargetsName();
+    if (targets.isEmpty()) {
+        return;
+    }
+
+    QMenu *subMenu = new QMenu(tr("Copy All As"), p_menu);
+    subMenu->setToolTipsVisible(true);
+    for (auto const & target : targets) {
+        QAction *act = new QAction(target, subMenu);
+        act->setData(target);
+        act->setToolTip(tr("Copy all content using rules specified by target %1").arg(target));
+
+        subMenu->addAction(act);
+    }
+
+    connect(subMenu, &QMenu::triggered,
+            this, &VWebView::handleCopyAllAsAction);
+
+    p_menu->addSeparator();
+    p_menu->addMenu(subMenu);
+}
+
+void VWebView::handleCopyAllAsAction(QAction *p_act)
+{
+    if (!p_act) {
+        return;
+    }
+
+    triggerPageAction(QWebEnginePage::SelectAll);
+
+    m_copyTarget = p_act->data().toString();
+
+    triggerPageAction(QWebEnginePage::Copy);
+
+    triggerPageAction(QWebEnginePage::Unselect);
+}
+
diff --git a/src/vwebview.h b/src/vwebview.h
index 453e912f..2db5b046 100644
--- a/src/vwebview.h
+++ b/src/vwebview.h
@@ -26,9 +26,9 @@ private slots:
 
     void handleCopyImageUrlAction();
 
-    void handleCopyWithoutBackgroundAction();
+    void handleCopyAsAction(QAction *p_act);
 
-    void handleCopyAllWithoutBackgroundAction();
+    void handleCopyAllAsAction(QAction *p_act);
 
     // Copy the clicked image.
     // Used to replace the default CopyImageToClipboard action.
@@ -41,24 +41,27 @@ private:
 
     void alterHtmlMimeData(QClipboard *p_clipboard,
                            const QMimeData *p_mimeData,
-                           bool p_removeBackground);
+                           const QString &p_copyTarget);
 
     void removeHtmlFromImageData(QClipboard *p_clipboard,
                                  const QMimeData *p_mimeData);
 
     bool removeStyles(QString &p_html);
 
+    void initCopyAsMenu(QAction *p_after, QMenu *p_menu);
+
+    void initCopyAllAsMenu(QMenu *p_menu);
+
     VFile *m_file;
 
     // Whether this view has hooked the Copy Image Url action.
     bool m_copyImageUrlActionHooked;
 
-    bool m_needRemoveBackground;
-
-    bool m_fixImgSrc;
-
     // Whether it is after copy image action.
     bool m_afterCopyImage;
+
+    // Target of Copy As.
+    QString m_copyTarget;
 };
 
 #endif // VWEBVIEW_H