在 nuxt 项目中实现一个 markdown 文本的渲染工作,可以用于微信公众号粘贴

公众号的排版时比较难搞的一件事,但是可以复制样式。因此很多人会采用在模板网站上调好文章的格式,然后复制粘贴到微信编辑器。
现在开源的几个我觉得样式比较一般,所以想做一个自己的,支持多主题切换的公众号排版工具。

正好在自己服务器搭了个 nuxt 服务,就作为一个模块写到里面吧。

# markdown 转换

markdown 原始的文本是无法被渲染的好看的,在浏览器中,使用工具按照规则将 markdown 转换为 html 标签,是更好的渲染方法。

  • 普通文本 =》p 标签;
  • 一级标题 =》h2 标签,其他标题同理;
  • 网址链接 =》a 标签
  • 代码 =》pre 标签
  • 无序列表 =》ul>li 标签
  • 有序列表 =》ol>li 标签
  • 图片 =》img 标签

在这个项目中,我选择 marked, 作为 markdown 转换为网页标签的转换器。
使用很简单方便:

  1. 安装
    npm install -g marked
  2. 使用 (我用的 nuxt3)
s
// 引入 其中 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 样式。

使用起来也很简单:

  1. 首先安装 @tailwindcss/typography 库
npm install -D @tailwindcss/typography
  1. 在 tailwind.config.js 中配置
module.exports = {
  theme: {
    // ...
  },
  plugins: [
    require('@tailwindcss/typography'),
    // ...
  ],
}
  1. 在想要的元素上给定一个 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,就不赘述了。

未完待续…