最近,一個朋友要求做一個數學編輯器,方便數學公式的錄入,特別是微積分、矩陣等公式,普通錄入非常麻煩,這里,花了一周時間,做了一個數學公式在線編輯功能。
下面記錄一下打造的過程。但是,目前很遺憾,這個系統還不支持導入導出功能。
如何實現web錄入的試題導出到word或者把word試題導入到系統,如果您有好的方法,歡迎推薦。(感覺要自己寫解析Latex)
在線體驗 http://demo.dotnetcms.org/math 免費下載 https://files.cnblogs.com/files/mqingqing123/math5.0.rar
1.MathJax
在數學公式里,最流行的是 http://www.mathjax.org ,Mathjax支持數理化等各種公式,其實如果你希望只針對數學錄入,可以使用 https://katex.org/ KaTex更簡單、速度更快。
Mathjax的文檔里列出了MathJax目前支持的LaTex語法。對於未實現的語法,可以自定義宏來實現。
從聲明里看到實現了 sin,cos,tan,ctan等都支持,但是一些反正切沒實現。
所以,在MathJax的全局配置里,定義一個macros
<script> MathJax = { options: { enableMenu: false, a11y: { speech: false, // switch on speech output braille: false, // switch on Braille output subtitles: false } }, tex: { inlineMath: [['@', '@'], ['\\(', '\\)']], displayMath: [['@@', '@@'], ['\\[', '\\]']], macros: { arcsec: '\\DeclareMathOperator{\\arcsec}{arcsec}\\arcsec', arccsc: '\\DeclareMathOperator{\\arccsc}{arccsc}\\arccsc', arccot: '\\DeclareMathOperator{\\arccot}{arccot}\\arccot' } } } </script>
然后引入Mathjax庫
<script src="../js/math/tex-chtml-full.js"></script>
另外,對於數學公式的“開始”和“結束”,MathJax默認使用"\("和"\)"作為分割的,
如果是塊狀的則使用"\\["和"\\]"區分,
參考下圖,左邊是錄入的內容,右邊是顯示的結果。

但是Mathjax允許你自定義公式識別符,
上面代碼,我增加了“@”作為行內公式,使用"@@"作為塊公式。
其實,在選型時,作者測試了“$”或者“#”作為分隔符,但是最終確定使用@符號,最根本的原因是:
在錄入時,只有@符號,在中英模式下是一樣的。
現在老師可以像寫文本一樣,寫題目了。

2.引入CodeMirror
在錄入頁面,引入Codemirror美化錄入界面。
畢竟,textarea默認太丑了。
<link href="../js/codeMirror/lib/codemirror.css" rel="stylesheet" /> <script src="../js/codeMirror/lib/codemirror.js"></script>
初始化文本框,整個布局分左右布局,
左邊是文本框textarea進入錄入,右邊是iframe進行預覽,
在父div里,設置display為flex,進行左右布局,這樣就不用 float 飛來飛去的了。
<div style="display:flex">
<div style="width:50%">
<textarea id="txt_question"></textarea>
</div>
<div style="width:50%; background-color:#f2f2f2">
<iframe id=preview frameborder="0"
width="100%"
scrolling="no" >
</iframe>
</div>
<script> var delay; var editor = CodeMirror.fromTextArea(document.getElementById('txt_question'), { lineNumbers: true, mode: 'text/html', lineWrapping:true }); editor.on("change", function () { clearTimeout(delay); delay = setTimeout(updatePreview, 500); }); function updatePreview() { var iframe = document.getElementById('preview'); var doc2 = iframe.contentDocument || iframe.contentWindow.document; let body2 = doc2.getElementsByTagName('body')[0]; var data = editor.getValue().replace(/\n/g, "<br>"); body2.innerHTML = "<div class=mathjax-qmx>" + data + "</div> "; if(doc2.defaultView.MathJax!=null) { doc2.defaultView.MathJax.typeset(); } } setTimeout(updatePreview, 500); </script>
在預覽時,需要通過JS引入Mathjax
<script>
$(document).ready(function () {
let iframe = document.getElementById("preview");
let iframeWindow = iframe.contentWindow || iframe.contentDocument.document || iframe.contentDocument;
let doc3 = iframeWindow.document;
let head3 = doc3.getElementsByTagName('head')[0];
let body3 = doc3.getElementsByTagName('body')[0];
let js1 = doc3.createElement('script');
js1.src = "../js/math/math-config.js";
js1.type = 'text/javascript';
head3.appendChild(js1);
let js2 = doc3.createElement('script');
js2.src = "../js/math/tex-mml-chtml.js";
js2.type = 'text/javascript';
js2.async = true;
js2.charset = 'utf-8';
head3.appendChild(js2);
});
</script>
最后使用codemirror提供的getValue可以獲取值。
另外,在預覽時,會把回車“\n”替換為“<br>”
var question = editor.getValue().replace(/\n/g, "<br>")+"";
這樣就可以獲取錄入的值。
3.打造菜單
為了方便錄入,打造了一個菜單,
菜單布局父class是math-menu,子菜單由sub-math-menu包裹。下面是HTML代碼
<div class="math-menu" data-editorid="editor">
<a href="###">菜單1</a>
<div class="sub-math-menu">
<span class="subnavbtn9">希臘字母 <span class="drop"></span> </span>
<div class="subnav-content9">
<div>小寫字母</div>
<a class="add" data-math="\alpha">@\alpha@</a>
<div style="clear:both"></div>
</div>
</div>
</div>
下圖是預覽效果。

下面是CSS樣式
.math-menu {
overflow: hidden;
background-color: #f2f2f2;
}
.math-menu a {
float: left;
font-size: 16px;
color: #000;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
.math-menu .sub-math-menu a {
font-size: 14px;
padding: 12px 14px;
}
.sub-math-menu {
float: left;
overflow: hidden;
}
.sub-math-menu .subnavbtn9 {
font-size: 16px;
border: none;
outline: none;
color: #000;
padding: 14px 16px;
background-color: inherit;
font-family: inherit;
margin: 0;
display:flex;
}
.math-menu a:hover, .sub-math-menu:hover .subnavbtn9 {
background-color: #ccc;
}
.subnav-content9 {
display: none;
position:absolute;
background-color: #ccc;
z-index: 1000;
left:12.5%;
width: 75%;
}
.subnav-content9 a {
float: left;
color: #000;
text-decoration: none;
height:50px;
}
.subnav-content9 a:hover {
background-color: #ffffff;
color: black;
}
.drop{
margin-top:10px;
margin-left:2px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 7px solid #333;
}
.CodeMirror {
border: 1px solid #eee;
height: 400px;
word-break:break-all;
font-family:Verdana;
}
.add{ cursor:pointer; }
.layui-card{ margin-bottom:15px; }
增加鼠標經過,菜單顯示效果。
注意:這里使用的是mouseover事件,而不是mouseenter事件。
<script>
$('.sub-math-menu').mouseover(function () {
$(this).find(".subnav-content9").show();
})
$('.sub-math-menu').mouseout(function () {
$(this).find(".subnav-content9").hide();
})
$(".add").click(
function ()
{
var ed= $(this).parent().parent().parent().data("editorid");
if(ed=="editor")
{
editor.replaceSelection("@"+$(this).data("math")+"@")
}
else
{
editor2.replaceSelection("@"+$(this).data("math")+"@")
}
$(this).parent().parent().find(".subnav-content9").hide();
}
);
</script>
到此,大功告成。
4.打造普通模式(小白模式)
當然,有時候你可能希望更多的控制,例如插入表格)
這里使用Tinymce集成Mathjax實現,其中,這里使用一個插件:https://github.com/dimakorotkov/tinymce-mathjax
代碼里,擴展了Tinymce菜單的定制。

默認這個插件提供的彈窗太小,可以放大,修改后代碼如下:
tinymce.PluginManager.add('mathjax', function(editor, url) {
// plugin configuration options
let mathjaxClassName = editor.settings.mathjax.className || "math-tex";
let mathjaxTempClassName = mathjaxClassName + '-original';
mathjaxSymbols = editor.settings.mathjax.symbols || { start: '\\(', end: '\\) ' };
let mathjaxUrl = editor.settings.mathjax.lib || null;
let mathjaxConfigUrl = (editor.settings.mathjax.configUrl || url + '/config.js') + '?class=' + mathjaxTempClassName;
let mathjaxScripts = [mathjaxConfigUrl];
if (mathjaxUrl) {
mathjaxScripts.push(mathjaxUrl);
}
// load mathjax and its config on editor init
editor.on('init', function () {
for (let i = 0; i < mathjaxScripts.length; i++) {
let id = editor.dom.uniqueId();
let script = editor.dom.create('script', {id: id, type: 'text/javascript', src: mathjaxScripts[i]});
editor.getDoc().getElementsByTagName('head')[0].appendChild(script);
}
});
// remove extra tags on get content
editor.on('GetContent', function (e) {
let div = editor.dom.create('div');
div.innerHTML = e.content;
let elements = div.querySelectorAll('.' + mathjaxClassName);
for (let i = 0; i < elements.length; i++) {
let children = elements[i].querySelectorAll('span');
for (let j = 0; j < children.length; j++) {
children[j].remove();
}
let latex = elements[i].getAttribute('data-latex');
elements[i].removeAttribute('contenteditable');
elements[i].removeAttribute('style');
elements[i].removeAttribute('data-latex');
elements[i].innerHTML = latex;
}
e.content = div.innerHTML;
});
let checkElement = function(element) {
if (element.childNodes.length != 2) {
element.setAttribute('contenteditable', false);
element.style.cursor = 'pointer';
let latex = element.getAttribute('data-latex') || element.innerHTML;
element.setAttribute('data-latex', latex);
element.innerHTML = '';
let math = editor.dom.create('span');
math.innerHTML = latex;
math.classList.add(mathjaxTempClassName);
element.appendChild(math);
let dummy = editor.dom.create('span');
dummy.classList.add('dummy');
dummy.innerHTML = 'dummy';
dummy.setAttribute('hidden', 'hidden');
element.appendChild(dummy);
}
};
// add dummy tag on set content
editor.on('BeforeSetContent', function (e) {
let div = editor.dom.create('div');
div.innerHTML = e.content;
let elements = div.querySelectorAll('.' + mathjaxClassName);
for (let i = 0 ; i < elements.length; i++) {
checkElement(elements[i]);
}
e.content = div.innerHTML;
});
// refresh mathjax on set content
editor.on('SetContent', function(e) {
if (editor.getDoc().defaultView.MathJax) {
editor.getDoc().defaultView.MathJax.startup.getComponents();
editor.getDoc().defaultView.MathJax.typeset();
}
});
// add button to tinimce
editor.ui.registry.addButton('插入公式', {
text: '插入公式',
tooltip: '插入公式',
onAction: function () {
openMathjaxEditor();
}
});
// handle click on existing
editor.on("click", function (e) {
let closest = e.target.closest('.' + mathjaxClassName);
if (closest) {
openMathjaxEditor(closest);
}
});
// open window with editor
let openMathjaxEditor = function(target) {
let mathjaxId = editor.dom.uniqueId();
let latex = '';
if (target) {
latex_attribute = target.getAttribute('data-latex');
if (latex_attribute.length >= (mathjaxSymbols.start + mathjaxSymbols.end).length) {
latex = latex_attribute.substr(mathjaxSymbols.start.length, latex_attribute.length - (mathjaxSymbols.start + mathjaxSymbols.end).length);
}
}
// show new window
editor.windowManager.open({
title: 'Mathjax',
size: 'medium',
body: {
type: 'panel',
items: [
{
type: 'htmlpanel',
html: '<div > <input onclick=changesybol() type=checkbox id=cb_br name=cb_br>換行 <a href="https://www.cnblogs.com/mqingqing123/p/12063096.html" target="blank" >LaTex說明</a> <a href="http://www.dotnetcms.org" target="blank" >啟明星官網</a> <style>.tox-textarea{height:150px !important; border-radius:0px;}</style> </div>'
},
{
type: 'textarea',
name: 'title'
},
{
type: 'htmlpanel',
html: '<iframe id="' + mathjaxId + '" style="width:98%; min-height: 50px; " ></iframe>'
}
]
},
buttons: [{ type: 'submit', text: '確定' }],
onSubmit: function onsubmit(api) {
let value = api.getData().title.trim();
if (target) {
target.innerHTML = '';
target.setAttribute('data-latex', getMathText(value));
checkElement(target);
} else {
let newElement = editor.getDoc().createElement('span');
newElement.innerHTML = getMathText(value);
newElement.classList.add(mathjaxClassName);
checkElement(newElement);
editor.insertContent(newElement.outerHTML);
}
editor.getDoc().defaultView.MathJax.startup.getComponents();
editor.getDoc().defaultView.MathJax.typeset();
api.close();
},
onChange: function(api) {
var value = api.getData().title.trim();
if (value != latex) {
refreshDialogMathjax(value, document.getElementById(mathjaxId));
latex = value;
}
},
initialData: {title: latex}
});
if (mathjaxSymbols.start == "\\(") {
document.getElementById("cb_br").checked = false;
}
else {
document.getElementById("cb_br").checked = true;
}
// add scripts to iframe
let iframe = document.getElementById(mathjaxId);
let iframeWindow = iframe.contentWindow || iframe.contentDocument.document || iframe.contentDocument;
let iframeDocument = iframeWindow.document;
let iframeHead = iframeDocument.getElementsByTagName('head')[0];
let iframeBody = iframeDocument.getElementsByTagName('body')[0];
// get latex for mathjax from simple text
let getMathText = function (value, symbols) {
if (!symbols) {
symbols = mathjaxSymbols;
}
return symbols.start + ' ' + value + ' ' + symbols.end ;
};
// refresh latex in mathjax iframe
let refreshDialogMathjax = function(latex) {
let MathJax = iframeWindow.MathJax;
let div = iframeBody.querySelector('div');
if (!div) {
div = iframeDocument.createElement('div');
div.classList.add(mathjaxTempClassName);
iframeBody.appendChild(div);
}
div.innerHTML = getMathText(latex, {start: '$$', end: '$$'});
if (MathJax && MathJax.startup) {
MathJax.startup.getComponents();
MathJax.typeset();
}
};
refreshDialogMathjax(latex);
// add scripts for dialog iframe
for (let i = 0; i < mathjaxScripts.length; i++) {
let node = iframeWindow.document.createElement('script');
node.src = mathjaxScripts[i];
node.type = 'text/javascript';
node.async = false;
node.charset = 'utf-8';
iframeHead.appendChild(node);
}
};
});
function changesybol() {
if (document.getElementById("cb_br").checked) {
mathjaxSymbols = { start: '\\[', end: '\\] ' };
}
else {
mathjaxSymbols = { start: '\\(', end: '\\) ' };
}
}
這樣,這個系統核心就完成了。
在線體驗 http://demo.dotnetcms.org/math
