Comi - 小程序 markdown 渲染和代碼高亮解決方案

寫在前面

Comi 讀 ['kəʊmɪ],相似中文 科米,是騰訊 Omi 團隊開發的小程序代碼高亮和 markdown 渲染組件。有了這個組件加持,小程序技術社區能夠開始搞起來了。javascript

體驗

感謝【小程序•雲開發】提供技術支持。css

預覽

Comi 基於下面的 5 個組件進行開發:html

  • prismjs
  • wxParse
  • remarkable
  • html2json
  • htmlparser

先看 Comi 使用,再分析原理。java

使用

先拷貝 此目錄 到你的項目。node

js:react

const comi = require('../../comi/comi.js');

Page({
  onLoad: function () {
    comi(`你要渲染的 md!`, this)
  }
})
複製代碼

wxml:git

<include src="../../comi/comi.wxml" />
複製代碼

wxss:github

@import "../../comi/comi.wxss";
複製代碼

簡單把!typescript

在 omip 中使用

先拷貝 此目錄 到你的項目。json

js:

import { WeElement, define } from 'omi'
import './index.css'
import comi from '../../components/comi/comi'

define('page-index', class extends WeElement {

  install() {
    comi(`你要渲染的 md`, this.$scope)
  }

  render() {
    return (
      <view> <include src="../../components/comi/comi.wxml" /> </view> ) } }) 複製代碼

WeElement 裏的 this 並非小程序裏的 this,須要使用 this.$scope 訪問小程序 Page或 Component 的 this。

css:

@import '../../components/comi/comi.wxss';
複製代碼

原理

在開發 Comi 以前,咱們進行了預研,是否有必要造這個輪子。

代碼高亮預研

  • wxParse 只是用標籤包括代碼,並未處理代碼轉成 WXML,因此渲染出的代碼是沒有顏色
  • 老牌的 highlightjs 沒有 WXML 對應的方案
  • 老牌的 highlightjs 對 JSX 高亮支持太差
  • prismjs 是 react 官方使用的高亮插件,對 JSX 支持高亮很好
  • prismjs 支持幾乎全部的語言,而且支持自定義擴展語言
  • prismjs 擁有 Line Highlight 插件(目前還未移植到 Comi)

綜合上面信息,決定基於 prismjs 二次開發。

markdown 渲染預研

  • wxParse 老牌的渲染組件,支持 markdown
  • wxParse 內置的 showdownjs 不知足代碼高亮的格式需求(好比語言種類也會生成一個標籤,固然能夠經過 wxss 隱藏)
  • 小程序基礎庫 1.4.0 開始支持 rich-text 組件展現富文本,可是格式須要轉成 json
  • 高性能 remarkable,Facebook 和 Docusaurus 都在使用,支持 md 語法修改和擴展
<rich-text nodes="{{nodes}}" bindtap="tap"></rich-text>
複製代碼
Page({
  data: {
    nodes: [{
      name: 'div',
      attrs: {
        class: 'div_class',
        style: 'line-height: 60px; color: red;'
      },
      children: [{
        type: 'text',
        text: 'Hello&nbsp;World!'
      }]
    }]
  },
  tap() {
    console.log('tap')
  }
})
複製代碼

綜合上面信息,放棄 rich-text,決定基於 wxParse + remarkable 二次開發,移除 showdownjs。Comi 須要 remarkable 的高性能和靈活性。markdown 會持久化存在 db, 在小程序內運行時轉換成 wxml,因此對性能仍是有必定要求。

劫持 prismjs tokens

tokens: function(text, grammar, language) {
  var env = {
    code: text,
    grammar: grammar,
    language: language
  };
  _.hooks.run('before-tokenize', env);
  env.tokens = _.tokenize(env.code, env.grammar);
  _.hooks.run('after-tokenize', env);

  for (var i = 0, len = env.tokens.length; i < len; i++) {
    var v = env.tokens[i]
    if (Object.prototype.toString.call(v.content) === '[object Array]') {
      v.deep = true this._walkContent(v.content)
    }
  }
  return env.tokens
},
複製代碼

這段代碼增長 tokens 方法到 prismjs 中,原庫自帶的 prism.highlight 的會把 tokens 轉成 html,由於咱們的目標的 wxml,因此這裏提早把 tokens 做爲方法返回值。固然還作了一件事,就是擴展了 token item 的 deep 屬性來決定是否須要繼續向下遍歷生成 wxml。

原始的 jsx:

render() {
    const { tks } = this.data
    return (
      <view class='pre language-jsx'> <view class='code'> {tks.map(tk => { return tk.deep ? <text class={'token ' + tk.type}>{ tk.content.map(stk => { return stk.deep ? stk.content.map(sstk => { return <text class={'token ' + sstk.type}>{sstk.content || sstk}</text> }) : <text class={'token ' + stk.type}>{stk.content || stk}</text> })}</text> : <text class={'token ' + tk.type}>{tk.content || tk}</text> })} </view> </view>
    )
  }
複製代碼

jsx 編譯出生成的 wxml,把這段 wxml 嵌入到 wxparse 裏:

<!-- 千萬 不要格式化下面的 wxml,否則 text 嵌套 text 致使換行所有出來了 -->
<template name="wxParseCode">
  <view class="pre language-jsx">
      <view class="code">
          <block wx:for="{{item.tks}}" wx:for-item="tk">
              <block wx:if="{{tk.deep}}"><text class="{{'token ' + tk.type}}"><block wx:for="{{tk.content}}" wx:for-item="stk"><block wx:if="{{stk.deep}}"><text class="{{'token ' + sstk.type}}" wx:for="{{stk.content}}" wx:for-item="sstk">{{sstk.content || sstk}}</text>
              </block>
              <block wx:else><text class="{{'token ' + stk.type}}">{{stk.content || stk}}</text>
              </block>
          </block>
          </text>
  </block>
  <block wx:else><text class="{{'token ' + tk.type}}">{{tk.content || tk}}</text>
  </block>
  </block>
  </view>
  </view>
</template>
複製代碼

這段 wxml 不能進行格式化美化,否則多出許多換行符,由於 text 嵌套 text 會保留換行符!!

修改 wxparse 裏的分支邏輯:

<block wx:elif="{{item.tagType == 'block'}}">
  <view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
    <block wx:if="{{item.tag == 'pre'}}">
        <template is="wxParseCode" data="{{item}}" />
    </block>
    <block wx:elif="{{item.tag != 'pre'}}" >
      <block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
        <template is="wxParse1" data="{{item}}" />
      </block>
    </block>
  </view>
</block>
複製代碼

item.tagpre 的時候使用 wxParseCode 模板,數據傳入 item。item 的數據從哪裏來?

先修改 md 渲染器爲 Remarkable:

} else if (type == 'md' || type == 'markdown') {
  var converter = new Remarkable()
  var html = converter.render(data)
  transData = HtmlToJson.html2json(html, bindName);
}
複製代碼

使用上面的 prism.tokens 計算出代碼片斷的 tokens,用於 wxparse 的模板渲染:

function transPre(transData) {
  transData.nodes.forEach((node, index) => {
    if (node.tag == 'pre') {
      var lan = 'markup'
      if (node.nodes[0].classStr) {
        lan = node.nodes[0].classStr.split(' ')[0].replace('language-', '')
      }
      var tks = prism.tokens(node.nodes[0].nodes[0].text, prism.languages[lan], lan)
      transData.nodes[index].tks = tks
    }
  })
}
複製代碼

language- 支持多少種呢?目前 comi 默認支持:

  • markup
  • css
  • clike
  • javascript
  • bash
  • json
  • typescript
  • jsx
  • tsx

默認使用的主題 css 是 okaidia。若是 comi 默認的配置不支持你的需求,你能夠:

  • prismjs.com/download.ht… 這裏自行下載
  • 劫持 prismjs tokens 拷貝進你下載的 prismjs 裏
  • 把 prismjs 拷貝替換掉 comi 自帶的 prismjs

精簡 comi 使用流程

WXML 提供兩種文件引用方式 import 和 include。和 import 不一樣,include 能夠將目標文件除了 template 和 wxs 外的整個代碼引入,至關因而拷貝到 include 位置,如:

<!-- index.wxml -->
<include src="header.wxml" />
<view>body</view>
<include src="footer.wxml" />
複製代碼
<!-- header.wxml -->
<view>header</view>
複製代碼
<!-- footer.wxml -->
<view>footer</view>
複製代碼

comi 利用了 import 和 include 特性簡化使用流程:

comi.wxml

<import src="./wxParse.wxml"/>
<template is="wxParse" data="{{wxParseData:article.nodes}}"/>
複製代碼

comi.js

var WxParse = require('./wxParse.js');

module.exports = function comi(md, scope) {
  WxParse.wxParse('article', 'md', md, scope, 5);
}
複製代碼

comi.wxss

@import './wxParse.wxss';
@import './prism.wxss';
複製代碼

使用時,只須要 :

  • import comi.js
  • include comi.wxml
  • import comi.wxss

另外,在 omip 使用 comi 時候發現不會拷貝 include 的文件到 dist,發現 taro/omip 的正則沒有去匹配 include 文件,因此,把:

exports.REG_WXML_IMPORT = /<[import](.*)?src=(?:(?:'([^']*)')|(?:"([^"]*)"))/gi
複製代碼

改爲:

exports.REG_WXML_IMPORT = /<[import|inculde](.*)?src=(?:(?:'([^']*)')|(?:"([^"]*)"))/gi
複製代碼

搞定。

開始使用吧

相關文章
相關標籤/搜索