feat: support custom code block theme (#112)

close #75
This commit is contained in:
miaogaolin 2021-12-03 14:35:25 +08:00 committed by GitHub
parent 10d5ce856b
commit fe4d6b0324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 107 additions and 255 deletions

5
.gitignore vendored
View File

@ -43,5 +43,8 @@ yarn-error.log*
# mockm # mockm
httpData httpData
package-lock.json
public/upload/** public/upload/**
!public/upload/*.gitkeep !public/upload/*.gitkeep

View File

@ -21,6 +21,7 @@
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"element-ui": "^2.15.6", "element-ui": "^2.15.6",
"form-data": "4.0.0", "form-data": "4.0.0",
"highlight.js": "^11.3.1",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"juice": "^8.0.0", "juice": "^8.0.0",
"marked": "^4.0.5", "marked": "^4.0.5",
@ -39,6 +40,7 @@
"@vue/cli-service": "~4.5.15", "@vue/cli-service": "~4.5.15",
"async-validator": "^4.0.7", "async-validator": "^4.0.7",
"babel-plugin-import": "^1.13.3", "babel-plugin-import": "^1.13.3",
"cache-loader": "^4.1.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jest": "^27.4.0", "jest": "^27.4.0",
"less": "^4.1.2", "less": "^4.1.2",

View File

@ -20,7 +20,7 @@ body,
/* 每个页面公共css */ /* 每个页面公共css */
@import url("./assets/less/style-mirror.css"); @import url("./assets/less/style-mirror.css");
@import url("./assets/less/theme.less"); @import url("./assets/less/theme.less");
@import url("./assets/less/code-theme.less");
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 6px;
height: 6px; height: 6px;

View File

@ -1,2 +0,0 @@
@import url("./codeTheme/wechat-code-block.less");
@import url("./codeTheme/github-code-block.less");

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -57,14 +57,29 @@ export default {
], ],
codeThemeOption: [ codeThemeOption: [
{ {
label: "微信", label: "github",
value: "wechat", value: "https://lib.baomitu.com/highlight.js/10.7.3/styles/github.min.css",
desc: "默认样式", desc: "light",
}, },
{ {
label: "GitHub", label: "solarized-light",
value: "github", value: "https://lib.baomitu.com/highlight.js/11.3.1/styles/base16/solarized-light.min.css",
desc: "精简风格", 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: { form: {

View File

@ -1,4 +1,5 @@
import { Renderer } from "marked"; import { Renderer } from "marked";
import hljs from 'highlight.js';
class WxRenderer { class WxRenderer {
constructor(opts) { constructor(opts) {
@ -7,9 +8,6 @@ class WxRenderer {
let footnoteIndex = 0; let footnoteIndex = 0;
let styleMapping = new Map(); let styleMapping = new Map();
const CODE_FONT_FAMILY =
"Menlo, Operator Mono, Consolas, Monaco, monospace";
let merge = (base, extend) => Object.assign({}, base, extend); let merge = (base, extend) => Object.assign({}, base, extend);
this.buildTheme = (themeTpl) => { 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) { for (let ele in themeTpl.block) {
if (themeTpl.block.hasOwnProperty(ele)) { if (themeTpl.block.hasOwnProperty(ele)) {
let style = themeTpl.block[ele]; let style = themeTpl.block[ele];
if (ele === "code") {
style["font-family"] = CODE_FONT_FAMILY;
}
mapping[ele] = merge(base_block, style); mapping[ele] = merge(base_block, style);
} }
} }
@ -126,23 +121,17 @@ class WxRenderer {
return `<blockquote ${getStyles("blockquote")}>${text}</blockquote>`; return `<blockquote ${getStyles("blockquote")}>${text}</blockquote>`;
}; };
renderer.code = (text, lang) => { renderer.code = (text, lang) => {
text = text.replace(/</g, "&lt;").replace(/>/g, "&gt;"); lang = hljs.getLanguage(lang) ? lang : 'plaintext';
const codeLines = text
.split("\n") text = hljs.highlight(text, {language: lang}).value;
.map(
(line) => text = text.replace(/\r\n/g,"<br/>")
`<code class="prettyprint"><span class="code-snippet_outer">${ .replace(/\n/g,"<br/>")
line || " " .replace(/(>[^<]+)|(^[^<]+)/g, function(str) {
}</span></code>` return str.replace(/\s/g, '&nbsp;')
); });
const codeTheme = "github";
return ` return `<pre class="hljs code__pre" ${getStyles("code_pre")}><code class="prettyprint language-${lang}" ${getStyles("code")}>${text}</code></pre>`
<section class="code-snippet__${codeTheme}">
<pre class="code__pre" data-lang="${lang}">
${codeLines.join("")}
</pre>
</section>
`;
}; };
renderer.codespan = (text, lang) => renderer.codespan = (text, lang) =>
`<code ${getStyles("codespan")}>${text}</code>`; `<code ${getStyles("codespan")}>${text}</code>`;

View File

@ -1,11 +1,9 @@
let baseColor = "#3f3f3f"
export default { export default {
BASE: { BASE: {
"text-align": "left", "text-align": "left",
color: "#3f3f3f", "line-height": "1.75"
"line-height": "1.75",
},
BASE_BLOCK: {
margin: "1em 8px",
}, },
block: { block: {
// 一级标题样式 // 一级标题样式
@ -17,6 +15,7 @@ export default {
margin: "2em auto 1em", margin: "2em auto 1em",
padding: "0 1em", padding: "0 1em",
"border-bottom": "2px solid rgba(0, 152, 116, 0.9)", "border-bottom": "2px solid rgba(0, 152, 116, 0.9)",
color: baseColor,
}, },
// 二级标题样式 // 二级标题样式
@ -39,6 +38,7 @@ export default {
"line-height": "1.2", "line-height": "1.2",
"padding-left": "8px", "padding-left": "8px",
"border-left": "3px solid rgba(0, 152, 116, 0.9)", "border-left": "3px solid rgba(0, 152, 116, 0.9)",
color: baseColor,
}, },
// 四级标题样式 // 四级标题样式
@ -53,6 +53,7 @@ export default {
p: { p: {
margin: "1.5em 8px", margin: "1.5em 8px",
"letter-spacing": "0.1em", "letter-spacing": "0.1em",
color: baseColor,
}, },
// 引用样式 // 引用样式
@ -72,20 +73,20 @@ export default {
"font-size": "1em", "font-size": "1em",
display: "block", display: "block",
}, },
code_pre: {
code: { "font-size": "14px",
"font-size": "80%", "overflow-x": "auto",
overflow: "auto",
color: "#333",
"white-space": "pre",
background: "rgb(247, 247, 247)",
"border-radius": "8px", "border-radius": "8px",
padding: "10px", padding: "1em",
"line-height": "1.5", "line-height": "1.5",
border: "1px solid rgb(236,236,236)", margin: "10px 8px"
margin: "20px 0",
}, },
code: {
"margin": 0,
"white-space": "nowrap",
"font-family": "Menlo, Operator Mono, Consolas, Monaco, monospace"
},
image: { image: {
"border-radius": "4px", "border-radius": "4px",
display: "block", display: "block",
@ -96,21 +97,25 @@ export default {
ol: { ol: {
"margin-left": "0", "margin-left": "0",
"padding-left": "1em", "padding-left": "1em",
color: baseColor,
}, },
ul: { ul: {
"margin-left": "0", "margin-left": "0",
"padding-left": "1em", "padding-left": "1em",
"list-style": "circle", "list-style": "circle",
color: baseColor,
}, },
footnotes: { footnotes: {
margin: "0.5em 8px", margin: "0.5em 8px",
"font-size": "80%", "font-size": "80%",
color: baseColor,
}, },
figure: { figure: {
margin: "1.5em 8px", margin: "1.5em 8px",
color: baseColor,
}, },
hr: { hr: {
"border-style": "solid", "border-style": "solid",
@ -127,6 +132,7 @@ export default {
"text-indent": "-1em", "text-indent": "-1em",
display: "block", display: "block",
margin: "0.2em 8px", margin: "0.2em 8px",
color: baseColor,
}, },
codespan: { codespan: {
@ -157,20 +163,24 @@ export default {
"border-collapse": "collapse", "border-collapse": "collapse",
"text-align": "center", "text-align": "center",
margin: "1em 8px", margin: "1em 8px",
color: baseColor,
}, },
thead: { thead: {
background: "rgba(0, 0, 0, 0.05)", background: "rgba(0, 0, 0, 0.05)",
"font-weight": "bold", "font-weight": "bold",
color: baseColor,
}, },
td: { td: {
border: "1px solid #dfdfdf", border: "1px solid #dfdfdf",
padding: "0.25em 0.5em", padding: "0.25em 0.5em",
color: baseColor,
}, },
footnote: { footnote: {
"font-size": "12px", "font-size": "12px",
color: baseColor,
}, },
figcaption: { figcaption: {

View File

@ -215,15 +215,6 @@ export function formatCss(content) {
return doc; 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 文档 * 导出原始 Markdown 文档
* @param {文档内容} doc * @param {文档内容} doc
@ -264,7 +255,6 @@ export function exportHTML() {
function setStyles(element) { function setStyles(element) {
switch (true) { switch (true) {
case isSection(element):
case isPre(element): case isPre(element):
case isCode(element): case isCode(element):
case isSpan(element): case isSpan(element):
@ -275,13 +265,6 @@ export function exportHTML() {
Array.from(element.children).forEach((child) => setStyles(child)); 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 元素 // 判断是否是包裹代码块的 pre 元素
function isPre(element) { function isPre(element) {
return ( return (

View File

@ -109,6 +109,22 @@
<span class="select-item-right">{{ color.desc }}</span> <span class="select-item-right">{{ color.desc }}</span>
</el-option> </el-option>
</el-select> </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-tooltip content="自定义颜色" :effect="effect" placement="top">
<el-color-picker <el-color-picker
v-model="selectColor" v-model="selectColor"
@ -205,7 +221,7 @@ export default {
selectFont: "", selectFont: "",
selectSize: "", selectSize: "",
selectColor: "", selectColor: "",
selectCodeTheme: "github", selectCodeTheme: config.codeThemeOption[0].value
}; };
}, },
components: { components: {
@ -275,7 +291,6 @@ export default {
setTimeout(() => { setTimeout(() => {
let clipboardDiv = document.getElementById("output"); let clipboardDiv = document.getElementById("output");
solveWeChatImage(); solveWeChatImage();
fixCodeWhiteSpace();
solveHtml(); solveHtml();
clipboardDiv.focus(); clipboardDiv.focus();
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
@ -286,7 +301,6 @@ export default {
window.getSelection().addRange(range); window.getSelection().addRange(range);
document.execCommand("copy"); document.execCommand("copy");
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
fixCodeWhiteSpace("normal");
clipboardDiv.innerHTML = this.output; clipboardDiv.innerHTML = this.output;
// //
this.$notify({ this.$notify({
@ -326,11 +340,13 @@ export default {
this.fontChanged(this.config.builtinFonts[0].value); this.fontChanged(this.config.builtinFonts[0].value);
this.colorChanged(this.config.colorOption[0].value); this.colorChanged(this.config.colorOption[0].value);
this.sizeChanged(this.config.sizeOption[2].value); this.sizeChanged(this.config.sizeOption[2].value);
this.codeThemeChanged(this.config.codeThemeOption[0].value)
this.$emit("cssChanged"); this.$emit("cssChanged");
this.selectFont = this.currentFont; this.selectFont = this.currentFont;
this.selectSize = this.currentSize; this.selectSize = this.currentSize;
this.selectColor = this.currentColor; this.selectColor = this.currentColor;
this.showResetConfirm = false; this.showResetConfirm = false;
this.selectCodeTheme = this.codeTheme;
}, },
cancelReset() { cancelReset() {
this.showResetConfirm = false; this.showResetConfirm = false;

View File

@ -149,6 +149,7 @@ export default {
currentColor: (state) => state.currentColor, currentColor: (state) => state.currentColor,
nightMode: (state) => state.nightMode, nightMode: (state) => state.nightMode,
rightClickMenuVisible: (state) => state.rightClickMenuVisible, rightClickMenuVisible: (state) => state.rightClickMenuVisible,
codeTheme: (state) => state.codeTheme,
}), }),
}, },
created() { created() {
@ -223,6 +224,21 @@ export default {
}); });
this.onEditorRefresh(); 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) { beforeUpload(file) {
// validate image // validate image
const checkResult = checkImage(file); const checkResult = checkImage(file);
@ -318,6 +334,7 @@ export default {
}, },
// //
onEditorRefresh() { onEditorRefresh() {
this.codeThemeChanged(this.codeTheme);
this.editorRefresh(); this.editorRefresh();
setTimeout(() => PR.prettyPrint(), 0); setTimeout(() => PR.prettyPrint(), 0);
}, },
@ -532,8 +549,10 @@ export default {
transform: none; transform: none;
} }
} }
.codeMirror-wrapper {
overflow-x: auto;
}
</style> </style>
<style lang="less" scoped> <style lang="less" scoped>
@import url("../../../assets/less/app.less"); @import url("../../../assets/less/app.less");
@import url("../../../assets/less/github-v2.min.css");
</style> </style>

View File

@ -21,7 +21,7 @@ const state = {
currentColor: "", currentColor: "",
citeStatus: 0, citeStatus: 0,
nightMode: false, nightMode: false,
codeTheme: "github", codeTheme: config.codeThemeOption[0].value,
rightClickMenuVisible: false, rightClickMenuVisible: false,
}; };
const mutations = { const mutations = {