简要介绍

利用marked,生成预览的过程中,缺少数学公式的支持。目前(2021.07)对数学公式支持比较好的库是katex。

以下实现主要参考两个地方:

  1. marked对katex集成问题的讨论
  2. marked的示例程序

实现原理:拦截marked的Renderer实现,然后调用katex.renderToString方法实现。

注意点:

  1. 拦截时,需要保存原来的实现类。见:const unchanged = new marked.Renderer()
  2. marked默认做了html encode,需要转一下,见方法:htmldecode()
  3. markdown的写法上
    1. 使用`$E=mc^2$`表示嵌入行内
    2. 使用`$$E=mc^2$$`表示在新段落中显示。

源代码示例

JavaScript部分(文件名:marked_with_katex.js)

// ============= add katex support =====================
// refer: https://github.com/markedjs/marked/issues/1538
// =====================================================
const renderer = new marked.Renderer();

var options = {
  renderer: renderer,
  highlight: function(code, lang) {
    const language = hljs.getLanguage(lang) ? lang : 'plaintext';
    return hljs.highlight(code, { language }).value;
  },
  pedantic: false,
  gfm: true,
  breaks: false,
  sanitize: false,
  smartLists: true,
  smartypants: false,
  xhtml: false
};
marked.setOptions(options);

function htmldecode(text){
    var temp = document.createElement("div");
    temp.innerHTML = text;
    var output = temp.innerText || temp.textContent;
    temp = null;
    return output;
}

function mathsExpression(expr) {
  if (expr.match(/^\$\$[\s\S]*\$\$$/)) {
    expr = expr.substr(2, expr.length - 4);
    return katex.renderToString(expr, { displayMode: true });
  } else if (expr.match(/^\$[\s\S]*\$$/)) {
    expr = htmldecode(expr); // temp solution
    expr = expr.substr(1, expr.length - 2);
    return katex.renderToString(expr, { displayMode: false });
  }
}

const unchanged = new marked.Renderer()

renderer.code = function(code, lang, escaped) {
  if (!lang) {
    const math = mathsExpression(code);
    if (math) {
      return math;
    }
  }
  return unchanged.code(code, lang, escaped);
};

renderer.codespan = function(text) {
  const math = mathsExpression(text);
  if (math) {
    return math;
  }
  return unchanged.codespan(text);
};

function markedWithKatex(text) {
    return  marked(text, options);
}
//const md = "hello```\n$$c=\sqrt{a^2 + b^2}$$\n```";
//console.log(marked(md, { renderer: renderer }));

完整的html(文件名demo2.html)

<!doctype html>
<html>
<head>
  <meta charset="utf-8"/>
  <title>Marked in the browser</title>
  <link rel="stylesheet" href="./demo.css" type="text/css" />
  <script src="https://cdn.jsdelivr.net/npm/marked@2.1.3/marked.min.js"></script> 

<link rel="stylesheet"
      href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.0.1/build/styles/default.min.css">
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.0.1/build/highlight.min.js"></script>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.css"> 
<script src="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.js" crossorigin="anonymous"></script>
<script src="marked_with_katex.js"></script>

</head>
<body>

    <div id="main">
      <div class="containers">
        <div class="container">
          <div class="label">Markdown <input  id="button" type="button" value="Click" > </div>
          <textarea id="markdown" class="inputPane"> 
markdown
</textarea>
        </div>

        <div class="container">
          <div class="label">
            <select id="outputType">
              <option value="preview">Preview</option>
              <option value="html">HTML Source</option>
              <option value="lexer">Lexer Data</option>
            </select> 
          </div>

          <div id="preview" class="pane">
             <iframe src="preview.html" frameborder="0" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts"></iframe>
          </div>
<!--
          <textarea id="html" class="pane" readonly="readonly"></textarea>
          <textarea id="lexer" class="pane" readonly="readonly"></textarea>
          <textarea id="quickref" class="pane" readonly="readonly"></textarea>
-->
        </div> <!-- container -->

      </div> <!-- containers -->

    </div> <!-- main -->

<script>

var previewIframe = document.querySelector('#preview iframe');
var markdownElem = document.querySelector('#markdown');

var delayTime = 1000;

var inputDirty = true;
function handleInput() {
  inputDirty = true;
}

markdownElem.addEventListener('change', handleInput, false);
markdownElem.addEventListener('keyup', handleInput, false);
markdownElem.addEventListener('keypress', handleInput, false);
markdownElem.addEventListener('keydown', handleInput, false);

function doParse() {
  if(inputDirty) {
      inputDirty = false;
      previewIframe.contentDocument.body.innerHTML = markedWithKatex(markdownElem.value);
  }
  checkChangeTimeout = window.setTimeout(doParse, delayTime);
}

var btn = document.getElementById("button");  
btn.onclick = function(){  
    doParse();
}

window.onload=function(){
    doParse();
}

</script>

</body>
</html>

iframe预览部分(文件名:preview.html)

<!doctype html>
<html>
<head>
  <meta charset="utf-8"/>
  <title>marked preview</title>
  <link rel="stylesheet" href="./demo.css" type="text/css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.css"> 

<link rel="stylesheet"
      href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.0.1/build/styles/default.min.css">
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.0.1/build/highlight.min.js"></script>

</head>
<body>
    hello marked
</body
</html>

css部分(仅供参考)(文件名:demo.css)

html, body {
	margin: 0;
	padding: 0;
	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
	color: #333;
	background-color: #fbfbfb;
	height: 100%;
}

textarea {
	font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
	font-size: 12px;
	resize: none;
}

header {
	padding-top: 10px;
	display: flex;
	height: 58px;
}

header h1 {
	margin: 0;
}

.containers {
	display: flex;
	height: calc(100vh - 68px);
}

.container {
	flex-basis: 50%;
	padding: 5px;
	display: flex;
	flex-direction: column;
	height: 100%;
	box-sizing: border-box;
}

.pane, .inputPane {
	margin-top: 5px;
	padding: 0.6em;
	border: 1px solid #ccc;
	overflow: auto;
	flex-grow: 1;
	flex-shrink: 1;
}

#preview {
	display: flex;
}

#preview iframe {
	flex-grow: 1;
}

.error {
	border-color: red;
	background-color: #FEE
}