From fe4d6b0324ce9ca6341c6dd593c4c38cd86dfc6f Mon Sep 17 00:00:00 2001
From: miaogaolin <57740293+miaogaolin@users.noreply.github.com>
Date: Fri, 3 Dec 2021 14:35:25 +0800
Subject: [PATCH] feat: support custom code block theme (#112)

close #75
---
 .gitignore                                    |  5 +-
 package.json                                  |  2 +
 src/App.vue                                   |  2 +-
 src/assets/less/code-theme.less               |  2 -
 .../less/codeTheme/github-code-block.less     | 49 -------------
 .../less/codeTheme/wechat-code-block.less     | 62 ----------------
 src/assets/less/github-v2.min.css             | 72 -------------------
 src/assets/scripts/config.js                  | 27 +++++--
 src/assets/scripts/renderers/wx-renderer.js   | 37 ++++------
 src/assets/scripts/themes/default-theme.js    | 42 ++++++-----
 src/assets/scripts/util.js                    | 17 -----
 src/components/CodemirrorEditor/header.vue    | 22 +++++-
 src/pages/index/view/CodemirrorEditor.vue     | 21 +++++-
 src/store/index.js                            |  2 +-
 14 files changed, 107 insertions(+), 255 deletions(-)
 delete mode 100644 src/assets/less/code-theme.less
 delete mode 100644 src/assets/less/codeTheme/github-code-block.less
 delete mode 100644 src/assets/less/codeTheme/wechat-code-block.less
 delete mode 100644 src/assets/less/github-v2.min.css

diff --git a/.gitignore b/.gitignore
index 9c387af19..7b3f1025a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,5 +43,8 @@ yarn-error.log*
 
 # mockm
 httpData
+
+package-lock.json
 public/upload/**
-!public/upload/*.gitkeep
\ No newline at end of file
+!public/upload/*.gitkeep
+
diff --git a/package.json b/package.json
index 25650a3e7..53f336e72 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
     "crypto-js": "^4.1.1",
     "element-ui": "^2.15.6",
     "form-data": "4.0.0",
+    "highlight.js": "^11.3.1",
     "jquery": "^3.6.0",
     "juice": "^8.0.0",
     "marked": "^4.0.5",
@@ -39,6 +40,7 @@
     "@vue/cli-service": "~4.5.15",
     "async-validator": "^4.0.7",
     "babel-plugin-import": "^1.13.3",
+    "cache-loader": "^4.1.0",
     "cross-env": "^7.0.3",
     "jest": "^27.4.0",
     "less": "^4.1.2",
diff --git a/src/App.vue b/src/App.vue
index 3c99dfdd5..58c415c01 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -20,7 +20,7 @@ body,
 /* 每个页面公共css */
 @import url("./assets/less/style-mirror.css");
 @import url("./assets/less/theme.less");
-@import url("./assets/less/code-theme.less");
+
 ::-webkit-scrollbar {
   width: 6px;
   height: 6px;
diff --git a/src/assets/less/code-theme.less b/src/assets/less/code-theme.less
deleted file mode 100644
index 2315e239a..000000000
--- a/src/assets/less/code-theme.less
+++ /dev/null
@@ -1,2 +0,0 @@
-@import url("./codeTheme/wechat-code-block.less");
-@import url("./codeTheme/github-code-block.less");
diff --git a/src/assets/less/codeTheme/github-code-block.less b/src/assets/less/codeTheme/github-code-block.less
deleted file mode 100644
index 0fb6e11d5..000000000
--- a/src/assets/less/codeTheme/github-code-block.less
+++ /dev/null
@@ -1,49 +0,0 @@
-@import url("../github-v2.min.css");
-/*github code block*/
-.code-snippet__github {
-  display: flex;
-  font-size: 12px;
-  margin: 10px 8px;
-  position: relative;
-  height: auto;
-  background-color: #f7f7f7;
-  border-radius: 8px;
-
-  .code-snippet__line-index {
-    display: none;
-  }
-
-  .code__pre {
-    display: grid;
-    position: relative;
-    counter-reset: line;
-    overflow-x: auto;
-    padding: 1em;
-    white-space: normal;
-    flex: 1;
-    line-height: 20px;
-    font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;
-    -webkit-overflow-scrolling: touch;
-  }
-
-  pre {
-    display: inline-block;
-    font-size: 12px;
-  }
-
-  code {
-    display: flex;
-    position: relative;
-    padding-right: 8px;
-    text-align: left;
-    white-space: pre;
-    font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
-    &::before {
-      display: none;
-    }
-  }
-
-  ul li {
-    list-style: none;
-  }
-}
diff --git a/src/assets/less/codeTheme/wechat-code-block.less b/src/assets/less/codeTheme/wechat-code-block.less
deleted file mode 100644
index 526559d7f..000000000
--- a/src/assets/less/codeTheme/wechat-code-block.less
+++ /dev/null
@@ -1,62 +0,0 @@
-/*wechat code block*/
-.rich_media_content .code-snippet *,
-.rich_media_content .code-snippet__wechat * {
-  max-width: 1000% !important;
-}
-
-.code-snippet__wechat {
-  word-wrap: break-word !important;
-  font-size: 14px;
-  margin: 10px 8px;
-  color: #333;
-  position: relative;
-  background-color: rgba(27, 31, 35, 0.05);
-  border: 1px solid #f0f0f0;
-  border-radius: 2px;
-  display: flex;
-  line-height: 24px;
-}
-
-.code-snippet__wechat .code-snippet__line-index {
-  counter-reset: line;
-  flex-shrink: 0;
-  height: 100%;
-  padding: 1em;
-  list-style-type: none;
-}
-
-.code-snippet__wechat .code-snippet__line-index li {
-  list-style-type: none;
-  text-align: right;
-}
-
-.code-snippet__wechat .code-snippet__line-index li::before {
-  min-width: 1.5em;
-  text-align: right;
-  left: -2.5em;
-  counter-increment: line;
-  content: counter(line);
-  display: inline;
-  color: rgba(0, 0, 0, 0.15);
-}
-
-.code-snippet__wechat pre {
-  overflow-x: auto;
-  padding: 1em 1em 1em 1em;
-  white-space: normal;
-  flex: 1;
-  -webkit-overflow-scrolling: touch;
-}
-
-.code-snippet__wechat code {
-  text-align: left;
-  font-size: 14px;
-  white-space: pre;
-  display: flex;
-  position: relative;
-  font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
-}
-
-.code-snippet__wechat ul li {
-  list-style: none;
-}
diff --git a/src/assets/less/github-v2.min.css b/src/assets/less/github-v2.min.css
deleted file mode 100644
index 3ff324cfe..000000000
--- a/src/assets/less/github-v2.min.css
+++ /dev/null
@@ -1,72 +0,0 @@
-/*! Color themes for Google Code Prettify | MIT License | github.com/jmblog/color-themes-for-google-code-prettify */
-.prettyprint {
-  font-family: Menlo, Bitstream Vera Sans Mono, DejaVu Sans Mono, Monaco,
-    Consolas, monospace;
-  border: 0 !important;
-}
-.pln {
-  color: #333;
-}
-ol.linenums {
-  margin-top: 0;
-  margin-bottom: 0;
-  color: #ccc;
-}
-li.L0,
-li.L1,
-li.L2,
-li.L3,
-li.L4,
-li.L5,
-li.L6,
-li.L7,
-li.L8,
-li.L9 {
-  padding-left: 1em;
-  background-color: #fff;
-  list-style-type: decimal;
-}
-@media screen {
-  .str {
-    color: #183691;
-  }
-  .kwd {
-    color: #a71d5d;
-  }
-  .com {
-    color: #969896;
-  }
-  .typ {
-    color: #0086b3;
-  }
-  .lit {
-    color: #0086b3;
-  }
-  .pun {
-    color: #333;
-  }
-  .opn {
-    color: #333;
-  }
-  .clo {
-    color: #333;
-  }
-  .tag {
-    color: navy;
-  }
-  .atn {
-    color: #795da3;
-  }
-  .atv {
-    color: #183691;
-  }
-  .dec {
-    color: #333;
-  }
-  .var {
-    color: teal;
-  }
-  .fun {
-    color: #900;
-  }
-}
diff --git a/src/assets/scripts/config.js b/src/assets/scripts/config.js
index ded88d71e..f6b794a8c 100644
--- a/src/assets/scripts/config.js
+++ b/src/assets/scripts/config.js
@@ -57,14 +57,29 @@ export default {
   ],
   codeThemeOption: [
     {
-      label: "微信",
-      value: "wechat",
-      desc: "默认样式",
+      label: "github",
+      value: "https://lib.baomitu.com/highlight.js/10.7.3/styles/github.min.css",
+      desc: "light",
     },
     {
-      label: "GitHub",
-      value: "github",
-      desc: "精简风格",
+      label: "solarized-light",
+      value: "https://lib.baomitu.com/highlight.js/11.3.1/styles/base16/solarized-light.min.css",
+      desc: "light",
+    },
+    {
+      label: "atom-one-dark",
+      value: "https://lib.baomitu.com/highlight.js/11.3.1/styles/atom-one-dark.min.css",
+      desc: "dark",
+    },
+    {
+      label: "obsidian",
+      value: "https://lib.baomitu.com/highlight.js/11.3.1/styles/obsidian.min.css",
+      desc: "dark",
+    },
+    {
+      label: "vs2015",
+      value: "https://lib.baomitu.com/highlight.js/11.3.1/styles/vs2015.min.css",
+      desc: "dark",
     },
   ],
   form: {
diff --git a/src/assets/scripts/renderers/wx-renderer.js b/src/assets/scripts/renderers/wx-renderer.js
index 46a1bdfdc..07cb9e0f3 100644
--- a/src/assets/scripts/renderers/wx-renderer.js
+++ b/src/assets/scripts/renderers/wx-renderer.js
@@ -1,4 +1,5 @@
 import { Renderer } from "marked";
+import hljs from 'highlight.js';
 
 class WxRenderer {
   constructor(opts) {
@@ -7,9 +8,6 @@ class WxRenderer {
     let footnoteIndex = 0;
     let styleMapping = new Map();
 
-    const CODE_FONT_FAMILY =
-      "Menlo, Operator Mono, Consolas, Monaco, monospace";
-
     let merge = (base, extend) => Object.assign({}, base, extend);
 
     this.buildTheme = (themeTpl) => {
@@ -25,13 +23,10 @@ class WxRenderer {
         }
       }
 
-      let base_block = merge(base, {});
+      let base_block = merge(base,  {});
       for (let ele in themeTpl.block) {
         if (themeTpl.block.hasOwnProperty(ele)) {
           let style = themeTpl.block[ele];
-          if (ele === "code") {
-            style["font-family"] = CODE_FONT_FAMILY;
-          }
           mapping[ele] = merge(base_block, style);
         }
       }
@@ -126,23 +121,17 @@ class WxRenderer {
         return `<blockquote ${getStyles("blockquote")}>${text}</blockquote>`;
       };
       renderer.code = (text, lang) => {
-        text = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
-        const codeLines = text
-          .split("\n")
-          .map(
-            (line) =>
-              `<code class="prettyprint"><span class="code-snippet_outer">${
-                line || " "
-              }</span></code>`
-          );
-        const codeTheme = "github";
-        return `
-                <section class="code-snippet__${codeTheme}">
-                    <pre class="code__pre" data-lang="${lang}">
-                        ${codeLines.join("")}
-                    </pre>
-                </section>
-            `;
+        lang = hljs.getLanguage(lang) ? lang : 'plaintext';
+
+        text = hljs.highlight(text, {language: lang}).value;
+
+        text = text.replace(/\r\n/g,"<br/>")
+                   .replace(/\n/g,"<br/>")
+                   .replace(/(>[^<]+)|(^[^<]+)/g, function(str) {
+                     return str.replace(/\s/g, '&nbsp;')
+                   });
+
+        return `<pre class="hljs code__pre" ${getStyles("code_pre")}><code class="prettyprint language-${lang}" ${getStyles("code")}>${text}</code></pre>`
       };
       renderer.codespan = (text, lang) =>
         `<code ${getStyles("codespan")}>${text}</code>`;
diff --git a/src/assets/scripts/themes/default-theme.js b/src/assets/scripts/themes/default-theme.js
index ccb22665b..f42d115cc 100644
--- a/src/assets/scripts/themes/default-theme.js
+++ b/src/assets/scripts/themes/default-theme.js
@@ -1,11 +1,9 @@
+let baseColor = "#3f3f3f"
+
 export default {
   BASE: {
     "text-align": "left",
-    color: "#3f3f3f",
-    "line-height": "1.75",
-  },
-  BASE_BLOCK: {
-    margin: "1em 8px",
+    "line-height": "1.75"
   },
   block: {
     // 一级标题样式
@@ -17,6 +15,7 @@ export default {
       margin: "2em auto 1em",
       padding: "0 1em",
       "border-bottom": "2px solid rgba(0, 152, 116, 0.9)",
+      color: baseColor,
     },
 
     // 二级标题样式
@@ -39,6 +38,7 @@ export default {
       "line-height": "1.2",
       "padding-left": "8px",
       "border-left": "3px solid rgba(0, 152, 116, 0.9)",
+      color: baseColor,
     },
 
     // 四级标题样式
@@ -53,6 +53,7 @@ export default {
     p: {
       margin: "1.5em 8px",
       "letter-spacing": "0.1em",
+      color: baseColor,
     },
 
     // 引用样式
@@ -72,20 +73,20 @@ export default {
       "font-size": "1em",
       display: "block",
     },
-
-    code: {
-      "font-size": "80%",
-      overflow: "auto",
-      color: "#333",
-      "white-space": "pre",
-      background: "rgb(247, 247, 247)",
+    code_pre: {
+      "font-size": "14px",
+      "overflow-x": "auto",
       "border-radius": "8px",
-      padding: "10px",
+      padding: "1em",
       "line-height": "1.5",
-      border: "1px solid rgb(236,236,236)",
-      margin: "20px 0",
+      margin: "10px 8px"
     },
-
+    code: {
+      "margin": 0,
+      "white-space": "nowrap",
+      "font-family": "Menlo, Operator Mono, Consolas, Monaco, monospace"
+    },
+  
     image: {
       "border-radius": "4px",
       display: "block",
@@ -96,21 +97,25 @@ export default {
     ol: {
       "margin-left": "0",
       "padding-left": "1em",
+      color: baseColor,
     },
 
     ul: {
       "margin-left": "0",
       "padding-left": "1em",
       "list-style": "circle",
+      color: baseColor,
     },
 
     footnotes: {
       margin: "0.5em 8px",
       "font-size": "80%",
+      color: baseColor,
     },
 
     figure: {
       margin: "1.5em 8px",
+      color: baseColor,
     },
     hr: {
       "border-style": "solid",
@@ -127,6 +132,7 @@ export default {
       "text-indent": "-1em",
       display: "block",
       margin: "0.2em 8px",
+      color: baseColor,
     },
 
     codespan: {
@@ -157,20 +163,24 @@ export default {
       "border-collapse": "collapse",
       "text-align": "center",
       margin: "1em 8px",
+      color: baseColor,
     },
 
     thead: {
       background: "rgba(0, 0, 0, 0.05)",
       "font-weight": "bold",
+      color: baseColor,
     },
 
     td: {
       border: "1px solid #dfdfdf",
       padding: "0.25em 0.5em",
+      color: baseColor,
     },
 
     footnote: {
       "font-size": "12px",
+      color: baseColor,
     },
 
     figcaption: {
diff --git a/src/assets/scripts/util.js b/src/assets/scripts/util.js
index d8f175472..44b48f8c7 100644
--- a/src/assets/scripts/util.js
+++ b/src/assets/scripts/util.js
@@ -215,15 +215,6 @@ export function formatCss(content) {
   return doc;
 }
 
-export function fixCodeWhiteSpace(value = "pre") {
-  const preDomList = document.getElementsByClassName("code__pre");
-  if (preDomList.length > 0) {
-    preDomList.forEach((pre) => {
-      pre.style.whiteSpace = value;
-    });
-  }
-}
-
 /**
  * 导出原始 Markdown 文档
  * @param {文档内容} doc
@@ -264,7 +255,6 @@ export function exportHTML() {
 
   function setStyles(element) {
     switch (true) {
-      case isSection(element):
       case isPre(element):
       case isCode(element):
       case isSpan(element):
@@ -275,13 +265,6 @@ export function exportHTML() {
       Array.from(element.children).forEach((child) => setStyles(child));
     }
 
-    // 判断是否是包裹代码块的 section 元素
-    function isSection(element) {
-      return (
-        element.tagName === "SECTION" &&
-        Array.from(element.classList).includes("code-snippet__github")
-      );
-    }
     // 判断是否是包裹代码块的 pre 元素
     function isPre(element) {
       return (
diff --git a/src/components/CodemirrorEditor/header.vue b/src/components/CodemirrorEditor/header.vue
index 4d7e610db..11c13921c 100644
--- a/src/components/CodemirrorEditor/header.vue
+++ b/src/components/CodemirrorEditor/header.vue
@@ -109,6 +109,22 @@
           <span class="select-item-right">{{ color.desc }}</span>
         </el-option>
       </el-select>
+      <el-select
+        v-model="selectCodeTheme"
+        size="mini"
+        placeholder="代码主题"
+        @change="codeThemeChanged"
+      >
+        <el-option
+          v-for="code in config.codeThemeOption"
+          :key="code.value"
+          :label="code.label"
+          :value="code.value"
+        >
+          <span class="select-item-left">{{ code.label }}</span>
+          <span class="select-item-right">{{ code.desc }}</span>
+        </el-option>
+      </el-select>
       <el-tooltip content="自定义颜色" :effect="effect" placement="top">
         <el-color-picker
           v-model="selectColor"
@@ -205,7 +221,7 @@ export default {
       selectFont: "",
       selectSize: "",
       selectColor: "",
-      selectCodeTheme: "github",
+      selectCodeTheme: config.codeThemeOption[0].value
     };
   },
   components: {
@@ -275,7 +291,6 @@ export default {
       setTimeout(() => {
         let clipboardDiv = document.getElementById("output");
         solveWeChatImage();
-        fixCodeWhiteSpace();
         solveHtml();
         clipboardDiv.focus();
         window.getSelection().removeAllRanges();
@@ -286,7 +301,6 @@ export default {
         window.getSelection().addRange(range);
         document.execCommand("copy");
         window.getSelection().removeAllRanges();
-        fixCodeWhiteSpace("normal");
         clipboardDiv.innerHTML = this.output;
         // 输出提示
         this.$notify({
@@ -326,11 +340,13 @@ export default {
       this.fontChanged(this.config.builtinFonts[0].value);
       this.colorChanged(this.config.colorOption[0].value);
       this.sizeChanged(this.config.sizeOption[2].value);
+      this.codeThemeChanged(this.config.codeThemeOption[0].value)
       this.$emit("cssChanged");
       this.selectFont = this.currentFont;
       this.selectSize = this.currentSize;
       this.selectColor = this.currentColor;
       this.showResetConfirm = false;
+      this.selectCodeTheme = this.codeTheme;
     },
     cancelReset() {
       this.showResetConfirm = false;
diff --git a/src/pages/index/view/CodemirrorEditor.vue b/src/pages/index/view/CodemirrorEditor.vue
index 98f0ae659..716ce55f5 100644
--- a/src/pages/index/view/CodemirrorEditor.vue
+++ b/src/pages/index/view/CodemirrorEditor.vue
@@ -149,6 +149,7 @@ export default {
       currentColor: (state) => state.currentColor,
       nightMode: (state) => state.nightMode,
       rightClickMenuVisible: (state) => state.rightClickMenuVisible,
+      codeTheme: (state) => state.codeTheme,
     }),
   },
   created() {
@@ -223,6 +224,21 @@ export default {
       });
       this.onEditorRefresh();
     },
+    // 切换 highlight.js 代码主题
+    codeThemeChanged() {
+      let cssUrl = this.codeTheme;
+      let el = document.getElementById('hljs')
+      if (el != undefined) {
+        el.setAttribute('href', cssUrl);
+      } else {
+        var link = document.createElement('link');
+        link.setAttribute('type','text/css');
+        link.setAttribute('rel','stylesheet');
+        link.setAttribute('href',cssUrl);
+        link.setAttribute('id','hljs');
+        document.head.appendChild(link);
+      }
+    },
     beforeUpload(file) {
       // validate image
       const checkResult = checkImage(file);
@@ -318,6 +334,7 @@ export default {
     },
     // 更新编辑器
     onEditorRefresh() {
+      this.codeThemeChanged(this.codeTheme);
       this.editorRefresh();
       setTimeout(() => PR.prettyPrint(), 0);
     },
@@ -532,8 +549,10 @@ export default {
     transform: none;
   }
 }
+.codeMirror-wrapper {
+  overflow-x: auto;
+}
 </style>
 <style lang="less" scoped>
 @import url("../../../assets/less/app.less");
-@import url("../../../assets/less/github-v2.min.css");
 </style>
diff --git a/src/store/index.js b/src/store/index.js
index 77b3c3fa4..d38456b90 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -21,7 +21,7 @@ const state = {
   currentColor: "",
   citeStatus: 0,
   nightMode: false,
-  codeTheme: "github",
+  codeTheme: config.codeThemeOption[0].value,
   rightClickMenuVisible: false,
 };
 const mutations = {