JS markdown 渲染

245 min read
var app = new Vue({
  el: '#app',
  data: function () {
    return {
      title: 'WeChat Format',
      aboutOutput: '',
      output: '',
      source: '',
      editorThemes: [
        { label: 'base16-light', value: 'base16-light' },
        { label: 'duotone-light', value: 'duotone-light' },
        { label: 'monokai', value: 'monokai' }
      ],
      currentEditorTheme: 'base16-light',
      editor: null,
      builtinFonts: [
        { label: '衬线', value: "Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif"},
        { label: '无衬线', value: "Roboto, Oxygen, Ubuntu, Cantarell, PingFangSC-light, PingFangTC-light, 'Open Sans', 'Helvetica Neue', sans-serif"}
      ],
      currentFont: "Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif",
      currentSize: '16px',
      sizeOption: [
        { label: '16px', value: '16px', desc: '默认' },
        { label: '17px', value: '17px', desc: '正常' },
        { label: '18px', value: '18px', desc: '稍大' }
      ],
      currentTheme: 'default',
      themeOption: [
        { label: 'default', value: 'default', author: 'Lyric'},
        { label: 'lupeng', value: 'lupeng', author: '鲁鹏'}
      ],
      styleThemes: {
        default: defaultTheme,
        lupeng: lupengTheme
      },
      aboutDialogVisible: false
    }
  },
  mounted () {
    var self = this
    this.editor = CodeMirror.fromTextArea(document.getElementById('editor'), {
      lineNumbers: false,
      lineWrapping: true,
      styleActiveLine: true,
      theme: this.currentEditorTheme,
      mode: 'text/x-markdown',
    });
    this.editor.on("change", function(cm, change) {
      self.refresh()
    })
    // this.currentFont = this.builtinFonts[0],
    this.wxRenderer = new WxRenderer({
      theme: this.styleThemes.default,
      fonts: this.currentFont,
      size: this.currentSize
    })
    axios({
      method: 'get',
      url: './assets/default-content.md',
    }).then(function (resp) {
      self.editor.setValue(resp.data)
    })
  },
  methods: {
    renderWeChat: function (source) {
      var output = marked(source, { renderer: this.wxRenderer.getRenderer() })
      if (this.wxRenderer.hasFootnotes()) {
        output += this.wxRenderer.buildFootnotes()
      }
      return output
    },
    editorThemeChanged: function (editorTheme) {
      this.editor.setOption('theme', editorTheme)
    },
    fontChanged: function (fonts) {
      this.wxRenderer.setOptions({
        fonts: fonts
      })
      this.refresh()
    },
    sizeChanged: function(size){
      this.wxRenderer.setOptions({
        size: size
      })
      this.refresh()
    },
    themeChanged: function(themeName){
      var themeName = themeName;
      var themeObject = this.styleThemes[themeName];
      this.wxRenderer.setOptions({
        theme: themeObject
      })
      this.refresh()
    },
    refresh: function () {
      this.output = this.renderWeChat(this.editor.getValue())
    },
    copy: function () {
      var clipboardDiv = document.getElementById('output')
      clipboardDiv.focus();
      window.getSelection().removeAllRanges();  
      var range = document.createRange(); 
      range.setStartBefore(clipboardDiv.firstChild);
      range.setEndAfter(clipboardDiv.lastChild);
      window.getSelection().addRange(range);  

      try {
        if (document.execCommand('copy')) {
          this.$message({
            message: '已复制到剪贴板', type: 'success'
          })
        } else {
          this.$message({
            message: '未能复制到剪贴板,请全选后右键复制', type: 'warning'
          })
        }
      } catch (err) {
        this.$message({
          message: '未能复制到剪贴板,请全选后右键复制', type: 'warning'
        })
      }
    }
  }
})

渲染代码

var WxRenderer = function (opts) {
  this.opts = opts
  var ENV_USE_REFERENCES = true
  var ENV_STETCH_IMAGE = true

  var footnotes = []
  var footnoteindex = 0
  var styleMapping = null

  var FONT_FAMILY_MONO = "Operator Mono, Consolas, Monaco, Menlo, monospace"

  var COPY = function (base, extend) { return Object.assign({}, base, extend)}

  this.buildTheme = function (themeTpl) {
    var mapping = {}
    var base = COPY(themeTpl.BASE, {
      'font-family': this.opts.fonts,
      'font-size': this.opts.size
    })
    var base_block = COPY(base, {
      'margin': '20px 10px'
    })
    for (var ele in themeTpl.inline) {
      if (themeTpl.inline.hasOwnProperty(ele)) {
        var style = themeTpl.inline[ele]
        if (ele === 'codespan') {
          style['font-family'] = FONT_FAMILY_MONO
        }
        mapping[ele] = COPY(base, style)
      }
    }
    for (var ele in themeTpl.block) {
      if (themeTpl.block.hasOwnProperty(ele)) {
        var style = themeTpl.block[ele]
        if (ele === 'code') {
          style['font-family'] = FONT_FAMILY_MONO
        }
        mapping[ele] = COPY(base_block, style)
      }
    }
    return mapping
  }

  var S = function (tokenName) {
    var arr = []
    var dict = styleMapping[tokenName]
    for (const key in dict) {
      arr.push(key + ':' + dict[key])
    }
    return 'style="' + arr.join(';') + '"'
  }

  var addFootnote = function (title, link) {
    footnoteindex += 1
    footnotes.push([footnoteindex, title, link])
    return footnoteindex
  }

  this.buildFootnotes = function () {
    var footnoteArray = footnotes.map(function (x) {
      if (x[1] === x[2]) {
        return '<code style="font-size: 90%; opacity: 0.6;">[' + x[0] + ']</code>: <i>'  + x[1] +'</i><br/>'
      }
      return '<code style="font-size: 90%; opacity: 0.6;">[' + x[0] + ']</code> ' + x[1] + ': <i>'  + x[2] +'</i><br/>'
    })
    return '<h3 ' + S('h3') + '>References</h3><p ' + S('footnotes') + '>'  + footnoteArray.join('\n') + '</p>'
  }

  this.setOptions = function (newOpts) {
    this.opts = COPY(this.opts, newOpts)
  }

  this.hasFootnotes = function () {
    return footnotes.length !== 0
  }

  this.getRenderer = function () {
    footnotes = []
    footnoteindex = 0
  
    styleMapping = this.buildTheme(this.opts.theme)
    var renderer = new marked.Renderer()
    FuriganaMD.register(renderer);
  
    renderer.heading = function (text, level) {
      if (level < 3) {
        return '<h2 ' + S('h2') + '>' + text + '</h2>'
      } else {
        return '<h3 ' + S('h3') + '>' + text + '</h3>'
      }
    }
    renderer.paragraph = function (text) {
      return '<p ' + S('p') + '>' + text + '</p>'
    }
    renderer.blockquote = function (text) {
      return '<blockquote ' + S('blockquote') + '>' + text + '</blockquote>'
    }
    renderer.code = function (text, infostring) {
      text = text.replace(/</g, "&lt;")
      text = text.replace(/>/g, "&gt;")
  
      var lines = text.split('\n')
      var codeLines = []
      var numbers = []
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i]
        codeLines.push('<code><span class="code-snippet_outer">' + (line || '<br>') + '</span></code>')
        numbers.push('<li></li>')
      }
      var lang = infostring || ''
      return '<section class="code-snippet__fix code-snippet__js">'
        + '<ul class="code-snippet__line-index code-snippet__js">' + numbers.join('')+'</ul>'
        + '<pre class="code-snippet__js" data-lang="'+lang+'">' 
          + codeLines.join('')
        + '</pre></section>'
    }
    renderer.codespan = function (text, infostring) {
      return '<code ' + S('codespan') + '>' + text + '</code>'
    }
    renderer.listitem = function (text) {
      return '<span ' + S('listitem') + '><span style="margin-right: 10px;"><%s/></span>' + text + '</span>';
    }
    renderer.list = function (text, ordered, start) {
      var segments = text.split('<%s/>');
      if (!ordered) {
        text = segments.join('•');
        return '<p ' + S('ul') + '>' + text + '</p>';
      }
      text = segments[0];
      for (var i = 1; i < segments.length; i++) {
        text = text + i + '.' + segments[i];
      }
      return '<p ' + S('ol') + '>' + text + '</p>';
    }
    renderer.image = function (href, title, text) {
      return '<img ' + S(ENV_STETCH_IMAGE ? 'image' : 'image_org') + ' src="' + href + '" title="'+title+'" alt="'+text+'"/>'
    }
    renderer.link = function (href, title, text) {
      if (href.indexOf('https://mp.weixin.qq.com') === 0) {
        return '<a href="' + href +'" title="' + (title || text) + '" ' + S('wx_link') +'>' + text + '</a>'; 
      }else if( href === text){
        return text;
      } else {
        if (ENV_USE_REFERENCES) {
          var ref = addFootnote(title || text, href)
          return '<span ' + S('link') + '>' + text + '<sup>['+ref+']</sup></span>'; 
        } else {
          return '<a href="' + href +'" title="' + (title || text) + '" ' + S('link') + '>' + text + '</a>'; 
        }
      }
    }
    renderer.strong = renderer.em = function (text) {
      return '<strong ' + S('strong') + '>' + text + '</strong>'; 
    }
    renderer.table = function (header, body) {
      return '<table ' + S('table') + '><thead ' + S('thead') + '>' + header + '</thead><tbody>' + body + '</tbody></table>'; 
    }
    renderer.tablecell = function (text, flags) {
      return '<td ' + S('td') + '>' + text + '</td>'; 
    }
    renderer.hr = function(){
      return '<hr style="border-style: solid;border-width: 1px 0 0;border-color: rgba(0,0,0,0.1);-webkit-transform-origin: 0 0;-webkit-transform: scale(1, 0.5);transform-origin: 0 0;transform: scale(1, 0.5);">';
    }
    return renderer
  }
}