十分钟打造一款在线的数学公式编辑器
最近,一个朋友要求做一个数学编辑器,方便数学公式的录入,特别是微积分、矩阵等公式,普通录入非常麻烦,这里,花了一周时间,做了一个数学公式在线编辑功能。
下面记录一下打造的过程。但是,目前很遗憾,这个系统还不支持导入导出功能。
如何实现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库
1 | <script src= "../js/math/tex-chtml-full.js" ></script> |
另外,对于数学公式的“开始”和“结束”,MathJax默认使用"
如果是块状的则使用"\["和"\]"区分,
参考下图,左边是录入的内容,右边是显示的结果。
但是Mathjax允许你自定义公式识别符,
上面代码,我增加了“@”作为行内公式,使用"@@"作为块公式。
其实,在选型时,作者测试了“$”或者“#”作为分隔符,但是最终确定使用@符号,最根本的原因是:
在录入时,只有@符号,在中英模式下是一样的。
现在老师可以像写文本一样,写题目了。
2.引入CodeMirror
在录入页面,引入Codemirror美化录入界面。
毕竟,textarea默认太丑了。
1 2 | <link href= "../js/codeMirror/lib/codemirror.css" rel= "stylesheet" /> <script src= "../js/codeMirror/lib/codemirror.js" ></script> |
初始化文本框,整个布局分左右布局,
左边是文本框textarea进入录入,右边是iframe进行预览,
在父div里,设置display为flex,进行左右布局,这样就不用 float 飞来飞去的了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <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> |
<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>
在预览时,需要通过JS引入Mathjax
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <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>”
1 | var question = editor.getValue().replace(/\n/g, "<br>" )+ "" ; |
这样就可以获取录入的值。
3.打造菜单
为了方便录入,打造了一个菜单,
菜单布局父class是math-menu,子菜单由sub-math-menu包裹。下面是HTML代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <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样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | .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事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <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菜单的定制。
默认这个插件提供的弹窗太小,可以放大,修改后代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2006-03-10 ASP.NET 快速入门系列系列--ASP.NET2.0绑定到数据库