zoukankan      html  css  js  c++  java
  • 十分钟打造一款在线的数学公式编辑器

    最近,一个朋友要求做一个数学编辑器,方便数学公式的录入,特别是微积分、矩阵等公式,普通录入非常麻烦,这里,花了一周时间,做了一个数学公式在线编辑功能。

    下面记录一下打造的过程。但是,目前很遗憾,这个系统还不支持导入导出功能。

    如何实现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="50%">
    <textarea id="txt_question"></textarea>
    </div>


    <div style="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(/ /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可以获取值。

    另外,在预览时,会把回车“ ”替换为“<br>”

      var question = editor.getValue().replace(/
    /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%;
       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;
         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="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

  • 相关阅读:
    Docker的镜像与容器
    【目标检测】YOLOv4中的Mish激活函数
    【深度学习】医学图像分割损失函数简介
    【深度学习】归一化方法
    【机器学习】Bagging与Boosting算法原理小结
    【目标检测】RCNN算法
    【机器学习】误差逆传播算法(反向传播算法)
    【机器学习】解决数据不平衡问题
    【干货总结】| Deep Reinforcement Learning 深度强化学习
    【深度学习】迁移学习Transfer Learning
  • 原文地址:https://www.cnblogs.com/mqingqing123/p/14509366.html
Copyright © 2011-2022 走看看