在 nuxt 项目中实现一个 markdown 文本的渲染工作,可以用于微信公众号粘贴
公众号的排版时比较难搞的一件事,但是可以复制样式。因此很多人会采用在模板网站上调好文章的格式,然后复制粘贴到微信编辑器。
现在开源的几个我觉得样式比较一般,所以想做一个自己的,支持多主题切换的公众号排版工具。
正好在自己服务器搭了个 nuxt 服务,就作为一个模块写到里面吧。
# markdown 转换
markdown 原始的文本是无法被渲染的好看的,在浏览器中,使用工具按照规则将 markdown 转换为 html 标签,是更好的渲染方法。
- 普通文本 =》p 标签;
- 一级标题 =》h2 标签,其他标题同理;
- 网址链接 =》a 标签
- 代码 =》pre 标签
- 无序列表 =》ul>li 标签
- 有序列表 =》ol>li 标签
- 图片 =》img 标签
在这个项目中,我选择 marked, 作为 markdown 转换为网页标签的转换器。
使用很简单方便:
- 安装
npm install -g marked - 使用 (我用的 nuxt3)
// 引入 其中 rawText 是原始的 md 文本,mdText 是转换后的 html 文本 | |
import {marked} from "marked"; | |
mdText.value = marked(props.rawText) |
/*使用v_html绑定转换后的md文本*/
<div v-html="mdText" id="preview" class="prose markdown" :class="classList"
:style="{fontSize:fontSize+'px'}"></div>
# taiwindcss 渲染问题
由于项目中引入了 tailwindcss,这是一个以类来定义样式的库,使用起来非常方便好用,同时我还用了 daisyui, 这是一个基于 taiwindcss 的样式库,使用体验,放到下一篇文章说吧。
tailwindcss 中包含对很多基本 html 标签的样式定义,比如 h1、a、等等,都会变成普通的文本格式,这样一来,转换后的 markdown 文本就看起来一点样式就没有了。
好在 tailwindcss 的设计者考虑到了这个问题,有些地方或者元素我们不希望 tailwind 的样式来影响,那么可以使用 @tailwindcss/typography 来为指定的元素取消 tailwind 样式。
使用起来也很简单:
- 首先安装 @tailwindcss/typography 库
npm install -D @tailwindcss/typography |
- 在 tailwind.config.js 中配置
module.exports = { | |
theme: { | |
// ... | |
}, | |
plugins: [ | |
require('@tailwindcss/typography'), | |
// ... | |
], | |
} |
- 在想要的元素上给定一个 class prose
<div v-html="mdText" id="preview" class="prose markdown" :class="classList"
:style="{fontSize:fontSize+'px'}"></div>
这样就 ok 了,原始的 html 样式就回来了。
# 渲染样式配置
作为配色障碍选手、设计小白,从零搭建一个好看的 markdown 主题不太现实,所以我参考(抄袭)了 typora 的一些开源主题。
# 样式切换
项目中使用 scss 作为 css 预处理语言。根据 typora 找来的主题,首先定义自己的一个类,比如 orangeheart 这个主题,它的原始 css 在这个地址:https://github.com/evgo2017/typora-theme-orange-heart/blob/master/orangeheart.css
复制到自己的项目中,并且去掉没有必要的名字或者参数:
.orangeheart{ | |
font-size: 1rem; | |
color: black; | |
padding: 0 10px; | |
line-height: 1.6; | |
word-spacing: 0px; | |
letter-spacing: 0px; | |
word-break: break-word; | |
word-wrap: break-word; | |
text-align: left; | |
font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; | |
/* 段落 */ | |
+ p, | |
blockquote p { | |
font-size: 1rem; | |
padding-top: .5rem; | |
padding-bottom: .5rem; | |
margin: 0; | |
line-height: 1.5rem; | |
color: black; | |
} | |
div[mdtype=toc] { | |
font-size: 1rem; | |
} | |
h1 h2 h3 h4 h5 h6 { | |
margin: 1.2em 0 1em; | |
padding: 0px; | |
font-weight: bold; | |
color: black; | |
} | |
h1 { | |
font-size: 1.5rem; | |
} | |
h2 { | |
font-size: 1.3rem; | |
border-bottom: 2px solid rgb(239, 112, 96); | |
} | |
h2 span { | |
display: inline-block; | |
font-weight: bold; | |
background: rgb(239, 112, 96); | |
color: #ffffff; | |
padding: 3px 10px 1px; | |
border-top-right-radius: 3px; | |
border-top-left-radius: 3px; | |
margin-right: 3px; | |
} | |
h2:after { | |
display: inline-block; | |
content: ""; | |
vertical-align: bottom; | |
border-bottom: 1.25rem solid #efebe9; | |
border-right: 1.25rem solid transparent; | |
} | |
h3 { | |
font-size: 1.3rem; | |
} | |
h4 { | |
font-size: 1.2rem; | |
} | |
h5 { | |
font-size: 1.1rem; | |
} | |
h6 { | |
font-size: 1rem; | |
} | |
/* 列表 */ | |
ul, | |
ol { | |
margin-top: 8px; | |
margin-bottom: 8px; | |
padding-left: 25px; | |
color: black; | |
} | |
ul { | |
list-style-type: disc; | |
} | |
ul ul { | |
list-style-type: square; | |
} | |
ol { | |
list-style-type: decimal; | |
} | |
li section { | |
margin-top: 5px; | |
margin-bottom: 5px; | |
line-height: 1.7rem; | |
text-align: left; | |
color: rgb(1,1,1); /* 只要是纯黑色微信编辑器就会把 color 这个属性吞掉。。。*/ | |
font-weight: 500; | |
} | |
/* 引用 */ | |
blockquote { | |
display: block; | |
font-size: .9em; | |
overflow: auto; | |
border-left: 3px solid rgb(239, 112, 96); | |
color: #6a737d; | |
padding: 10px 10px 10px 20px; | |
margin-bottom: 20px; | |
margin-top: 20px; | |
background: #fff9f9; | |
} | |
/* 链接 */ | |
a { | |
text-decoration: none; | |
word-wrap: break-word; | |
font-weight: bold; | |
border-bottom: 1px solid #1e6bb8; | |
color: rgb(239, 112, 96); | |
border-bottom: 1px solid rgb(239, 112, 96); | |
} | |
/* 行内代码 */ | |
p code, | |
li code { | |
font-size: .9rem; | |
word-wrap: break-word; | |
padding: 2px 4px; | |
border-radius: 4px; | |
margin: 0 2px; | |
color: rgb(239, 112, 96);; | |
background-color: rgba(27,31,35,.05); | |
font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; | |
word-break: break-all; | |
} | |
/* 图片 */ | |
img { | |
display: block; | |
margin: 0 auto; | |
max-width: 100%; | |
} | |
span img { | |
display: inline-block; | |
border-right: 0px; | |
border-left: 0px; | |
} | |
/* 表格 */ | |
table { | |
display: table; | |
text-align: left; | |
} | |
tbody { | |
border: 0; | |
} | |
table tr { | |
border: 0; | |
border-top: 1px solid #ccc; | |
background-color: white; | |
} | |
table tr:nth-child(2n) { | |
background-color: #F8F8F8; | |
} | |
table tr th, | |
table tr td { | |
font-size: 1rem; | |
border: 1px solid #ccc; | |
padding: 5px 10px; | |
text-align: left; | |
} | |
table tr th { | |
font-weight: bold; | |
background-color: #f0f0f0; | |
} | |
/* 行内代码 */ | |
span code, li code { | |
color: rgb(239, 112, 96); | |
} | |
/* 脚注上标 */ | |
.md-footnote { | |
font-weight: bold; | |
color: rgb(239, 112, 96); | |
} | |
.md-footnote > .md-text:before { | |
content: '[' | |
} | |
.md-footnote > .md-text:after { | |
content: ']' | |
} | |
/* 脚注 */ | |
.md-def-name { | |
padding-right: 1.8ch; | |
} | |
.md-def-name:before { | |
content: '['; | |
color: #000; | |
} | |
.md-def-name:after { | |
color: #000; | |
} | |
/* 代码块主题 */ | |
.md-fences:before { | |
content: ' '; | |
display: block; | |
width: 100%; | |
background-size: 40px; | |
background-repeat: no-repeat; | |
background-color: #282c34; | |
margin-bottom: -7px; | |
border-radius: 5px; | |
background-position: 10px 10px; | |
} | |
/* CodeMirror 相关内容 */ | |
.CodeMirror-wrap .CodeMirror-scroll { | |
overflow-x: auto; | |
} | |
.cm-s-inner.CodeMirror { | |
padding: .5rem; | |
background-color: #292d3e; | |
color: #a6accd; | |
font-family: Consolas; | |
border-radius: 4px; | |
} | |
.cm-s-inner .cm-keyword { | |
color: #c792ea !important; | |
} | |
.cm-s-inner .cm-operator { | |
color: #89ddff !important; | |
} | |
.cm-s-inner .cm-variable-2 { | |
color: #eeffff !important; | |
} | |
.cm-s-inner .cm-variable-3, | |
.cm-s-inner .cm-type { | |
color: #f07178 !important; | |
} | |
.cm-s-inner .cm-builtin { | |
color: #ffcb6b !important; | |
} | |
.cm-s-inner .cm-atom { | |
color: #f78c6c !important; | |
} | |
.cm-s-inner .cm-number { | |
color: #ff5370 !important; | |
} | |
.cm-s-inner .cm-def { | |
color: #82aaff !important; | |
} | |
.cm-s-inner .cm-string { | |
color: #c3e88d !important; | |
} | |
.cm-s-inner .cm-string-2 { | |
color: #f07178 !important; | |
} | |
.cm-s-inner .cm-comment { | |
color: #676e95 !important; | |
} | |
.cm-s-inner .cm-variable { | |
color: #f07178 !important; | |
} | |
.cm-s-inner .cm-tag { | |
color: #ff5370 !important; | |
} | |
.cm-s-inner .cm-meta { | |
color: #ffcb6b !important; | |
} | |
.cm-s-inner .cm-attribute { | |
color: #c792ea !important; | |
} | |
.cm-s-inner .cm-property { | |
color: #c792ea !important; | |
} | |
.cm-s-inner .cm-qualifier { | |
color: #decb6b !important; | |
} | |
.cm-s-inner .cm-variable-3, | |
.cm-s-inner .cm-type { | |
color: #decb6b !important; | |
} | |
.cm-s-inner .cm-error { | |
color: rgba(255, 255, 255, 1) !important; | |
background-color: #ff5370 !important; | |
} | |
.cm-s-inner .CodeMirror-matchingbracket { | |
text-decoration: underline; | |
color: white !important; | |
} | |
.CodeMirror div.CodeMirror-cursor { | |
border-left: 1px solid rgb(239, 112, 96); | |
z-index: 3; | |
} | |
.cm-s-inner div.CodeMirror-selected { | |
background: rgba(167, 178, 189, 0.2) !important; | |
} | |
.cm-s-inner.CodeMirror-focused div.CodeMirror-selected { | |
background: rgba(167, 178, 189, 0.2) !important; | |
} | |
.cm-s-inner .CodeMirror-selected, | |
.cm-s-inner .CodeMirror-selectedtext { | |
background-color: rgba(167, 178, 189, 0.0) !important; | |
} | |
.cm-s-inner .CodeMirror-line::-moz-selection, | |
.cm-s-inner .CodeMirror-line > span::-moz-selection, | |
.cm-s-inner .CodeMirror-line > span > span::-moz-selection { | |
background-color: rgba(167, 178, 189, 0.2); | |
} | |
.cm-s-inner .CodeMirror-line::selection, | |
.cm-s-inner .CodeMirror-line > span::selection, | |
.cm-s-inner .CodeMirror-line > span > span::selection { | |
background-color: rgba(167, 178, 189, 0.2); | |
} | |
} |
这里定义了类名为 orangeheart,然后再 nuxt (nuxt.config.ts)中需要引入定义的样式;
export default defineNuxtConfig({ | |
... | |
css:[ | |
'@/assets/css/main.css', | |
'@/assets/css/font.css', | |
'@/assets/css/wechat_theme/index.scss', | |
], | |
... | |
}) |
然后回到 md 的容器,添加 class: orangeheart 一切 ok 啦,样式出现啦,当然了,觉得不好看,可以自己再调整;
接下来,再从 typora 偷
几个主题,分别给不同的 class 名,将 class 动态绑定到元素上,这里就不再罗嗦了,相信用过 vue 的都知道咋搞了。
# 字体切换
从 100font 找了几个免费的字体库,将里面的 ttf 文件放到我们的项目中,定义字体样式类:
.deyihei{ | |
@font-face { | |
font-family: 'deyihei'; | |
font-weight: normal; | |
src: url('../../font/deyihei.ttf') format('truetype'); | |
} | |
font-family: deyihei !important; | |
} | |
.sanjipomo{ | |
@font-face { | |
font-family: 'sanjipomo'; | |
font-weight: normal; | |
src: url('../../font/三极泼墨体.ttf') format('truetype'); | |
} | |
font-family: sanjipomo !important; | |
} | |
.zibangwankuti{ | |
@font-face { | |
font-family: 'zibangwankuti'; | |
font-weight: normal; | |
src: url('../../font/字帮玩酷体.ttf') format('truetype'); | |
} | |
font-family: zibangwankuti !important; | |
} |
注意
:
- 这里使用了!import 提高字体的优先级,否则可能因为已有的 font-family 而不生效。
定义了三个字体类之后,还是可以动态绑定的方式给到 md 容器,这里也省略吧,别忘了默认字体。
# 字体大小切换
这个就更简单了,只是一个动态绑定的 style,就不赘述了。
未完待续…