如何封装一个支持LaTex的Markdown解析器

909 阅读3分钟

起因

最近在仿写一款优秀的写作软件,优秀的写作软件支持markdown的解析自然是一个必不可少的功能,在思考如何实现解析器的同时,恰好阅读了钟颖大佬出品的关于“代码编辑器”的技术文章,发现可以使用 WebView封装 markdown-ithighlightjs.org 一类的web项目,低成本地实现一个有着不错效果的CommonMark解析器。

实现过程

1. HTML文件创建

创建本地文件 index.html,并输入代码。页面中用到的js文件和资源需要后续创建和生成。<body>标签中唯一的<div>将用于后续插入通过 markdown-it渲染的代码,页面的样式布局通过css文件实现和优化。

<html>
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
        <link rel="stylesheet" href="./main.css" />
        <script src="./main.js"></script>
    </head>
    <body>
        <div class="container" id="contents"></div>
    </body>
</html>

2. JS文件创建

创建本地文件 index.js,在文件中编写需要注入网页的js代码。

import hljs from 'highlight.js'
import MarkdownIt from 'markdown-it'
import emoji from 'markdown-it-emoji'
import './../css/bootstrap.css'
import './../css/gist.css'
import './../css/github.css'
import './../css/index.css'

window.showMarkdown = (percentEncodedMarkdown, enableImage = true) => {

  if (!percentEncodedMarkdown) {
    return
  }

  const markdownText = decodeURIComponent(percentEncodedMarkdown)

  let markdown = new MarkdownIt({
    html: true,
    breaks: true,
    linkify: true,
    highlight: function(code){
        return hljs.highlightAuto(code).value;
    }
  })
  if (!enableImage) {
    markdown = markdown.disable('image')
  }
  markdown.use(emoji)
  markdown.use(require('markdown-it-latex2img'))
  let html = markdown.render(markdownText)

  document.getElementById('contents').innerHTML = html
  let tables = document.querySelectorAll('table')
  tables.forEach((table) => {
    table.classList.add('table')
  })
  let codes = document.querySelectorAll('pre code')
  codes.forEach((code) => {
    hljs.highlightBlock(code)
  })

}

主要用到的js库有:

markdown-it:100% CommonMark 语法解析
highlight.js:代码高亮
markdown-it-emoji:emoji语法支持
markdown-it-latex2img:基于服务器端的MathJax解析器

代码的基本逻辑主要是: 1.由于markdown格式的内容被传入时事先进行了编码操作,所以这里需要调用decodeURIComponent()函数对内容先进行解码。 2.使用 markdown-it别结合所需插件对内容进行渲染,更多关于 markdown-it的使用和插件支持,可以参考官方的API文档:《markdown-it 中文文档》。 3.将渲染后的代码插入 index.html页面的对应位置,并支持表格(内嵌功能)和代码高亮的显示。

3. 控件封装

业务逻辑:创建 WKWebView,并注入js代码(showMarkdown()函数调用) 关键代码

  @objc public func load(markdown: String?, enableImage: Bool = true) {
    guard let markdown = markdown else { return }

    if let url = htmlURL {
      let templateRequest = URLRequest(url: url)

      let escapedMarkdown = self.escape(markdown: markdown) ?? ""
      let imageOption = enableImage ? "true" : "false"
      let script = "window.showMarkdown('\(escapedMarkdown)', \(imageOption));"
      let userScript = WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true)

      let controller = WKUserContentController()
      controller.addUserScript(userScript)

      let configuration = WKWebViewConfiguration()
      configuration.userContentController = controller

      let wv = WKWebView(frame: self.bounds, configuration: configuration)
      wv.scrollView.isScrollEnabled = self.isScrollEnabled
      wv.translatesAutoresizingMaskIntoConstraints = false
      wv.navigationDelegate = self
      addSubview(wv)
    
      // 代码省略:布局约束、空间样式
      // ...

      wv.load(templateRequest)
    } else {
      // TODO: raise error
    }
  }

  private func escape(markdown: String) -> String? {
    return markdown.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics)
  }

4. 资源文件打包

到此,代码层面的工作就可以结束了。 最后我们需要使用 Webpack工具将js应用程序打包成方便使用的静态资源,输出名为 main.js的文件,将其放置在 index.html文件相同的目录下。这里提供一个可供参考的配置文件:webpack.config.js

写在最后

本文所描述的实现过程,主要参考了三方库 MarkdownView,因为需要支持LaTeX的解析,我稍微重写了showMarkdown函数,加入了 markdown-it-latex2img库的使用,能够将数学公式以图片的形式进行展示。过程中,还学习了 Webpack工具的使用和js静态资源的打包。Webview项目的封装能够为原生提供更丰富的功能,能够为复杂的业务实现提供更多的思路和扩展,希望本文能够对需要的人有所帮助。

参考资料:

Taio开发笔记
Webpack教程
markdown-it API文档
keitaoouchi/MarkdownView