[LON-CAPA-cvs] cvs: loncom /build compressjs.sh /homework response.pm /html/adm/LC_math_editor build.sh test.html /html/adm/LC_math_editor/dist LC_math_editor.min.js /html/adm/LC_math_editor/src definitions.js enode.js operator.js parse_exception.js parser.js token.js tokenizer.js ui.js doc/loncapafiles loncapafiles.lpml

damieng damieng at source.lon-capa.org
Wed Sep 24 14:14:39 EDT 2014


damieng		Wed Sep 24 18:14:39 2014 EDT

  Added files:                 
    /loncom/build	compressjs.sh 
    /loncom/html/adm/LC_math_editor	build.sh test.html 
    /loncom/html/adm/LC_math_editor/dist	LC_math_editor.min.js 
    /loncom/html/adm/LC_math_editor/src	definitions.js enode.js 
                                       	operator.js parse_exception.js 
                                       	parser.js token.js tokenizer.js 
                                       	ui.js 

  Modified files:              
    /doc/loncapafiles	loncapafiles.lpml 
    /loncom/homework	response.pm 
  Log:
  added new LON-CAPA math editor for mathresponse and formularesponse to replace the DragMath applet
  
-------------- next part --------------
Index: doc/loncapafiles/loncapafiles.lpml
diff -u doc/loncapafiles/loncapafiles.lpml:1.897 doc/loncapafiles/loncapafiles.lpml:1.898
--- doc/loncapafiles/loncapafiles.lpml:1.897	Wed Aug 20 18:02:12 2014
+++ doc/loncapafiles/loncapafiles.lpml	Wed Sep 24 18:14:19 2014
@@ -2,7 +2,7 @@
  "http://lpml.sourceforge.net/DTD/lpml.dtd">
 <!-- loncapafiles.lpml -->
 
-<!-- $Id: loncapafiles.lpml,v 1.897 2014/08/20 18:02:12 raeburn Exp $ -->
+<!-- $Id: loncapafiles.lpml,v 1.898 2014/09/24 18:14:19 damieng Exp $ -->
 
 <!--
 
@@ -613,6 +613,12 @@
 </directory>
 <directory dist='default'>
   <protectionlevel>modest_delete</protectionlevel>
+  <targetdir dist='default'>home/httpd/html/adm/LC_math_editor </targetdir>
+  <categoryname>server readonly</categoryname>
+  <description>LON-CAPA math editor</description>
+</directory>
+<directory dist='default'>
+  <protectionlevel>modest_delete</protectionlevel>
   <targetdir dist='default'>home/httpd/html/ckeditor</targetdir>
   <categoryname>server readonly</categoryname>
   <description>Rich Text Editor</description>
@@ -4650,6 +4656,19 @@
 </file>
 
 <fileglob>
+  <glob>*.js</glob>
+  <sourcedir>loncom/html/adm/dragmath/dist/</sourcedir>
+  <targetdir dist='default'>home/httpd/html/adm/LC_math_editor/</targetdir>
+  <categoryname>interface file</categoryname>
+  <description>
+LON-CAPA math editor
+  </description>
+  <filenames>
+LC_math_editor.min.js
+  </filenames>
+</fileglob>
+
+<fileglob>
   <glob>*.*</glob>
   <sourcedir>loncom/html/adm/jsMath/</sourcedir>
   <targetdir dist='default'>home/httpd/html/adm/jsMath/</targetdir>
Index: loncom/homework/response.pm
diff -u loncom/homework/response.pm:1.236 loncom/homework/response.pm:1.237
--- loncom/homework/response.pm:1.236	Thu Aug 28 14:41:18 2014
+++ loncom/homework/response.pm	Wed Sep 24 18:14:27 2014
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # various response type definitons response definition
 #
-# $Id: response.pm,v 1.236 2014/08/28 14:41:18 raeburn Exp $
+# $Id: response.pm,v 1.237 2014/09/24 18:14:27 damieng Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -578,10 +578,15 @@
 
 sub edit_mathresponse_button {
     my ($id,$field)=@_;
-    my $button=&mt('Edit Answer');
-#    my $helplink=&Apache::loncommon::help_open_topic('Formula_Editor');
-    my $iconpath=$Apache::lonnet::perlvar{'lonIconsURL'};
-    return(<<ENDFORMULABUTTON);
+    my $btype = $env{'browser.type'};
+    my $bversion = $env{'browser.version'};
+    if (($btype eq 'explorer' && $bversion < 9) || ($btype eq 'safari' && $bversion < 3) ||
+        ($btype eq 'mozilla' && $bversion < 3)) {
+      # DragMath applet
+      my $button=&mt('Edit Answer');
+#     my $helplink=&Apache::loncommon::help_open_topic('Formula_Editor');
+      my $iconpath=$Apache::lonnet::perlvar{'lonIconsURL'};
+      return(<<ENDFORMULABUTTON);
 <script type="text/javascript" language="JavaScript">
 function edit_${id}_${field} (textarea) {
     thenumber = textarea;
@@ -591,6 +596,27 @@
 </script>
 <a href="javascript:edit_${id}_${field}('${field}');void(0);"><img class="stift" src="$iconpath/stift.gif" alt="$button" title="$button" /></a>
 ENDFORMULABUTTON
+      
+    } else {
+      # LON-CAPA math equation editor
+      return(<<EQ_EDITOR_SCRIPT);
+<script type="text/javascript">
+  var field = document.getElementById('${field}');
+  field.className += ' math'; // note the space
+  var LCMATH_started;
+  if (typeof LCMATH_started === 'undefined') {
+    LCMATH_started = true;
+    var script = document.createElement("script");
+    script.type = "text/javascript";
+    script.src = "/adm/LC_math_editor/LC_math_editor.min.js";
+    document.body.appendChild(script);
+    window.addEventListener('load', function(e) {
+        LCMATH.initEditors();
+    }, false);
+  }
+</script>
+EQ_EDITOR_SCRIPT
+    }
 }
 
 sub end_mathresponse {

Index: loncom/build/compressjs.sh
+++ loncom/build/compressjs.sh
#!/bin/bash

# Constants
SERVICE_URL=http://closure-compiler.appspot.com/compile
NEWFILE="c`date +"%d%m%y"`.js"
OPTIONS=""

if [ "$1" = "-strict" ]
then
        OPTIONS="--data language=ECMASCRIPT5_STRICT"
        shift
fi

# Check if files to compile are provided
if [ $# -eq 0 ]
then
	echo 'Nothing to compile. Specify input files as command arguments. E.g.'
	echo './compressjs file1.js file2.js file3.js'
	exit
fi

# Itearate through all files
for f in $*
do
	if [ -r "${f}" ]
	then
		code="${code} --data-urlencode js_code@${f}"
	else
		echo "File ${f} does not exist or is not readable. Skipped."
	fi
done

# Send request
curl \
--url ${SERVICE_URL} \
--header 'Content-type: application/x-www-form-urlencoded' \
${code} \
--data output_format=json \
--data output_info=compiled_code \
--data output_info=statistics \
--data output_info=errors \
--data compilation_level=SIMPLE_OPTIMIZATIONS \
${OPTIONS} |

python -c '
import json, sys
data = json.load(sys.stdin)

if "errors" in data:
	print "### COMPILATION FAILED WITH ERRORS"
	for err in data["errors"]:
		file = sys.argv[int(err["file"].replace("Input_", "")) + 1]
		print "File: %s, %d:%d" % (file, err["lineno"], err["charno"])
		print "Error: %s" % err["error"]
		print "Line: %s" % err["line"]
		
	print "\nBuild failed.\n"
	
else:
	print "### COMPILATION COMPLETED"
	print "Original size: %db, gziped: %db" % (data["statistics"]["originalSize"], data["statistics"]["originalGzipSize"])
	print "Compressed size: %db, gziped: %db" % (data["statistics"]["compressedSize"], data["statistics"]["compressedGzipSize"])
	print "Compression rate: %.2f" % (float(data["statistics"]["compressedSize"]) / int(data["statistics"]["originalSize"]))

	filename = "'${NEWFILE}'"
	f = open(filename, "w")
	f.write(data["compiledCode"])

	print "\nBuild file %s created.\n" % filename
' $@
Index: loncom/html/adm/LC_math_editor/build.sh
+++ loncom/html/adm/LC_math_editor/build.sh
#!/bin/bash

# this builds dist/LC_math_editor.min.js using google closure compiler with compressjs.
# It must be run in the math_editor directory.

cd "$(dirname "$0")"
mkdir -p dist
tmp=$(mktemp)
cat <<% >$tmp
"use strict";
var LCMATH = function () {
%
cat src/*.js >>$tmp
cat <<% >>$tmp
    return({
        "Definitions": Definitions,
        "ENode": ENode,
        "Operator": Operator,
        "ParseException": ParseException,
        "Parser": Parser,
        "initEditors": initEditors
    });
}();
%

NEWFILE="c`date +"%d%m%y"`.js"
/bin/sh ../../../build/compressjs.sh -strict $tmp
mv "$NEWFILE" ./dist/LC_math_editor.min.js
rm -f $tmp

Index: loncom/html/adm/LC_math_editor/test.html
+++ loncom/html/adm/LC_math_editor/test.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Math editor test</title>
    <style>
        div.eqnbox { margin: 1em }
        textarea.math { font-family: monospace; height: 3em; width: 100%; }
        span.math-error { border: solid 1px red; min-width: 1px; }
    </style>
</head>
<body>
    <p>Strict syntax, symbolic mode:</p>
    <div class="eqnbox">
        <textarea class="math" spellcheck="false" autofocus="autofocus"></textarea>
    </div>
    <p>Strict syntax, unit mode (no variable):</p>
    <div class="eqnbox">
        <textarea class="math" data-unit_mode="true" data-constants="c, pi, e, hbar, amu" spellcheck="false" autofocus="autofocus"></textarea>
    </div>
    <p>Lax syntax, symbolic mode:</p>
    <div class="eqnbox">
        <textarea class="math" data-implicit_operators="true" spellcheck="false" autofocus="autofocus"></textarea>
    </div>
    <p>Lax syntax, unit mode:</p>
    <div class="eqnbox">
        <textarea class="math" data-implicit_operators="true" data-unit_mode="true" data-constants="c, pi, e, hbar, amu" spellcheck="false" autofocus="autofocus"></textarea>
    </div>
    <div class="eqnbox">
        Test in a field <input class="math" data-implicit_operators="true" spellcheck="false" autofocus="autofocus"></textarea> with text around (Lax syntax, symbolic mode)
    </div>
    <script src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=MML_HTMLorMML"></script>
    <script src="src/definitions.js"></script>
    <script src="src/enode.js"></script>
    <script src="src/operator.js"></script>
    <script src="src/parse_exception.js"></script>
    <script src="src/parser.js"></script>
    <script src="src/token.js"></script>
    <script src="src/tokenizer.js"></script>
    <script src="src/ui.js"></script>
    <script>
        window.addEventListener('load', function(e) {
            initEditors(); // will be LCMATH.init_editors() with the minimized version
        }, false);
    </script>
</body>
</html>

Index: loncom/html/adm/LC_math_editor/dist/LC_math_editor.min.js
+++ loncom/html/adm/LC_math_editor/dist/LC_math_editor.min.js
'use strict';var LCMATH=function(){function k(){this.operators=[]}function e(a,b,c,f){this.type=a;this.op=b;this.value=c;this.children=f}function n(a,b,c,f,e,g){this.id=a;this.arity=b;this.lbp=c;this.rbp=f;this.nud=e;this.led=g}function p(a,b,c){this.msg=a;this.from=b;this.to=c?c:this.from}function r(a,b,c){this.implicit_operators="undefined"==typeof a?!1:a;this.unit_mode="undefined"==typeof b?!1:b;this.constants="undefined"==typeof c?[]:c;this.defs=new k;this.defs.define();this.operators=this.defs.operators;
this.oph={};for(a=0;a<this.operators.length;a++)this.oph[this.operators[a].id]=this.operators[a]}function s(a,b){this.defs=a;this.text=b}function m(a,b,c,f,e){this.type=a;this.from=b;this.to=c;this.value=f;this.op=e}k.ARG_SEPARATOR=";";k.DECIMAL_SIGN_1=".";k.DECIMAL_SIGN_2=",";k.prototype.operator=function(a,b,c,f,e,g){this.operators.push(new n(a,b,c,f,e,g))};k.prototype.separator=function(a){this.operator(a,n.BINARY,0,0,null,null)};k.prototype.infix=function(a,b,c,f){var l;l=n.BINARY;f=f||function(b,
d){var f=[d,b.expression(c)];return new e(e.OPERATOR,this,a,f)};this.operator(a,l,b,c,null,f)};k.prototype.prefix=function(a,b,c){var f;f=n.UNARY;c=c||function(c){c=[c.expression(b)];return new e(e.OPERATOR,this,a,c)};this.operator(a,f,0,b,c,null)};k.prototype.suffix=function(a,b,c){var f;f=n.UNARY;c=c||function(b,c){return new e(e.OPERATOR,this,a,[c])};this.operator(a,f,b,0,null,c)};k.prototype.findOperator=function(a){for(var b=0;b<this.operators.length;b++)if(this.operators[b].id==a)return this.operators[b];
return null};k.prototype.define=function(){this.suffix("!",160);this.infix("^",140,139);this.infix(".",130,129);this.infix("`",125,125,function(a,b){for(var c=a.expression(125);null!=a.current_token&&-1!="*/".indexOf(a.current_token.value);){var f=a.tokens[a.token_nr];if(null==f)break;if(f.type!=m.NAME&&"("!=f.value)break;f=a.tokens[a.token_nr+1];if(null!=f&&("("==f.value||f.type==m.NUMBER))break;if(a.unit_mode&&a.tokens[a.token_nr].type==m.NAME){for(var f=a.tokens[a.token_nr].value,l=!1,g=0;g<a.constants.length;g++)if(f==
a.constants[g]){l=!0;break}if(l)break}f=a.current_token;a.advance();c=f.op.led(a,c)}return new e(e.OPERATOR,this,"`",[b,c])});this.infix("*",120,120);this.infix("/",120,120);this.infix("%",120,120);this.infix("+",100,100);this.operator("-",n.BINARY,100,134,function(a){a=[a.expression(134)];return new e(e.OPERATOR,this,"-",a)},function(a,b){var c=[b,a.expression(100)];return new e(e.OPERATOR,this,"-",c)});this.infix("=",80,80);this.infix("#",80,80);this.infix("<=",80,80);this.infix(">=",80,80);this.infix("<",
80,80);this.infix(">",80,80);this.separator(")");this.separator(k.ARG_SEPARATOR);this.operator("(",n.BINARY,200,200,function(a){var b=a.expression(0);a.advance(")");return b},function(a,b){if(b.type!=e.NAME&&b.type!=e.SUBSCRIPT)throw new p("Function name expected before a parenthesis.",a.tokens[a.token_nr-1].from);var c=[b];if(null==a.current_token||null==a.current_token.op||")"!==a.current_token.op.id)for(;;){c.push(a.expression(0));if(null==a.current_token||null==a.current_token.op||a.current_token.op.id!==
k.ARG_SEPARATOR)break;a.advance(k.ARG_SEPARATOR)}a.advance(")");return new e(e.FUNCTION,this,"(",c)});this.separator("]");this.operator("[",n.BINARY,200,70,function(a){var b=[];if(null==a.current_token||null==a.current_token.op||"]"!==a.current_token.op.id)for(;;){b.push(a.expression(0));if(null==a.current_token||null==a.current_token.op||a.current_token.op.id!==k.ARG_SEPARATOR)break;a.advance(k.ARG_SEPARATOR)}a.advance("]");return new e(e.VECTOR,this,null,b)},function(a,b){if(b.type!=e.NAME&&b.type!=
e.SUBSCRIPT)throw new p("Name expected before a square bracket.",a.tokens[a.token_nr-1].from);var c=[b];if(null==a.current_token||null==a.current_token.op||"]"!==a.current_token.op.id)for(;;){c.push(a.expression(0));if(null==a.current_token||null==a.current_token.op||a.current_token.op.id!==k.ARG_SEPARATOR)break;a.advance(k.ARG_SEPARATOR)}a.advance("]");return new e(e.SUBSCRIPT,this,"[",c)})};e.UNKNOWN=0;e.NAME=1;e.NUMBER=2;e.OPERATOR=3;e.FUNCTION=4;e.VECTOR=5;e.SUBSCRIPT=6;e.COLORS="#E01010 #0010FF #009000 #FF00FF #00B0B0 #F09000 #800080 #F080A0 #6090F0 #902000 #70A050 #A07060 #5000FF #E06050 #008080 #808000".split(" ");
e.prototype.toString=function(){var a="(";switch(this.type){case e.UNKNOWN:a+="UNKNOWN";break;case e.NAME:a+="NAME";break;case e.NUMBER:a+="NUMBER";break;case e.OPERATOR:a+="OPERATOR";break;case e.FUNCTION:a+="FUNCTION";break;case e.VECTOR:a+="VECTOR";break;case e.SUBSCRIPT:a+="SUBSCRIPT"}this.op&&(a+=" '"+this.op.id+"'");this.value&&(a+=" '"+this.value+"'");if(this.children){for(var a=a+" [",b=0;b<this.children.length;b++)a+=this.children[b].toString(),b!=this.children.length-1&&(a+=",");a+="]"}return a+
")"};e.prototype.getColorForIdentifier=function(a,b){var c=b[a];c||(c=e.COLORS[Object.keys(b).length%e.COLORS.length],b[a]=c);return c};e.prototype.toMathML=function(a){var b,c,f,l,g,d,h;"undefined"==typeof a&&(a={hcolors:{},depth:0});b=null!=this.children&&0<this.children.length?this.children[0]:null;c=null!=this.children&&1<this.children.length?this.children[1]:null;f=null!=this.children&&2<this.children.length?this.children[2]:null;l=null!=this.children&&3<this.children.length?this.children[3]:
null;g=null!=this.children&&4<this.children.length?this.children[4]:null;switch(this.type){case e.UNKNOWN:return d=document.createElement("mtext"),d.appendChild(document.createTextNode("???")),d;case e.NAME:return 0<=this.value.search(/^[a-zA-Z]+[0-9]+$/)?(d=this.value.search(/[0-9]/),f=document.createElement("msub"),f.appendChild(this.mi(this.value.substring(0,d))),f.appendChild(this.mn(this.value.substring(d))),d=f):d=this.mi(this.value),d.setAttribute("mathcolor",this.getColorForIdentifier(this.value,
a.hcolors)),d;case e.NUMBER:return-1!=this.value.indexOf("e")||-1!=this.value.indexOf("E")?(a=this.value.indexOf("e"),-1==a&&(a=this.value.indexOf("E")),d=document.createElement("mrow"),d.appendChild(this.mn(this.value.substring(0,a))),d.appendChild(this.mo("\u22c5")),b=document.createElement("msup"),b.appendChild(this.mn(10)),b.appendChild(this.mn(this.value.substring(a+1))),d.appendChild(b),d):this.mn(this.value);case e.OPERATOR:if("/"==this.value)g=document.createElement("mfrac"),g.appendChild(b.toMathML(a)),
g.appendChild(c.toMathML(a)),d=g;else if("^"==this.value)f=b.type==e.FUNCTION?"sqrt"==b.value||"abs"==b.value||"matrix"==b.value||"diff"==b.value?!1:!0:b.type==e.OPERATOR?!0:!1,d=document.createElement("msup"),f?d.appendChild(this.addP(b,a)):d.appendChild(b.toMathML(a)),d.appendChild(c.toMathML(a));else if("*"==this.value){d=document.createElement("mrow");b.type!=e.OPERATOR||"+"!=b.value&&"-"!=b.value?d.appendChild(b.toMathML(a)):d.appendChild(this.addP(b,a));for(f=c;f.type==e.OPERATOR;)f=f.children[0];
b.type==e.VECTOR&&c.type==e.VECTOR?d.appendChild(this.mo("*")):f.type==e.NUMBER&&d.appendChild(this.mo("\u22c5"));c.type!=e.OPERATOR||"+"!=c.value&&"-"!=c.value?d.appendChild(c.toMathML(a)):d.appendChild(this.addP(c,a))}else if("-"==this.value)d=document.createElement("mrow"),1==this.children.length?(d.appendChild(this.mo("-")),d.appendChild(b.toMathML(a))):(d.appendChild(b.toMathML(a)),d.appendChild(this.mo("-")),c.type!=e.OPERATOR||"+"!=c.value&&"-"!=c.value?d.appendChild(c.toMathML(a)):d.appendChild(this.addP(c,
a)));else if("!"==this.value)d=document.createElement("mrow"),h=this.mo(this.value),b.type!=e.OPERATOR||"+"!=b.value&&"-"!=b.value?d.appendChild(b.toMathML(a)):d.appendChild(this.addP(b,a)),d.appendChild(h);else if("+"==this.value){d=document.createElement("mrow");h=this.mo(this.value);d.appendChild(b.toMathML(a));d.appendChild(h);f=!1;for(l=c;l.type==e.OPERATOR;)if("-"==l.value&&1==l.children.length){f=!0;break}else if("+"==l.value||"-"==l.value||"*"==l.value)l=l.children[0];else break;f?d.appendChild(this.addP(c,
a)):d.appendChild(c.toMathML(a))}else"."==this.value?(d=document.createElement("mrow"),b.type!=e.OPERATOR||"+"!=b.value&&"-"!=b.value?d.appendChild(b.toMathML(a)):d.appendChild(this.addP(b,a)),d.appendChild(this.mo("\u22c5")),c.type!=e.OPERATOR||"+"!=c.value&&"-"!=c.value?d.appendChild(c.toMathML(a)):d.appendChild(this.addP(c,a))):"`"==this.value?(d=document.createElement("mrow"),b.type!=e.OPERATOR||"+"!=b.value&&"-"!=b.value?d.appendChild(b.toMathML(a)):d.appendChild(this.addP(b,a)),f=document.createElement("mstyle"),
f.setAttribute("fontstyle","normal"),c.type!=e.OPERATOR||"+"!=c.value&&"-"!=c.value?f.appendChild(c.toMathML(a)):f.appendChild(this.addP(c,a)),d.appendChild(f)):(d=document.createElement("mrow"),h=this.mo(this.value),d.appendChild(b.toMathML(a)),d.appendChild(h),d.appendChild(c.toMathML(a)));return d;case e.FUNCTION:if("sqrt"==b.value&&null!=c)d=document.createElement("msqrt"),d.appendChild(c.toMathML(a));else if("abs"==b.value&&null!=c)d=document.createElement("mrow"),d.appendChild(this.mo("|")),
d.appendChild(c.toMathML(a)),d.appendChild(this.mo("|"));else if("exp"==b.value&&null!=c)d=document.createElement("msup"),d.appendChild(this.mi("e")),d.appendChild(c.toMathML(a));else if("factorial"==b.value)d=document.createElement("mrow"),h=this.mo("!"),c.type!=e.OPERATOR||"+"!=c.value&&"-"!=c.value?d.appendChild(c.toMathML(a)):d.appendChild(this.addP(c,a)),d.appendChild(h);else if("diff"==b.value&&null!=this.children&&3==this.children.length)d=document.createElement("mrow"),g=document.createElement("mfrac"),
g.appendChild(this.mi("d")),h=document.createElement("mrow"),h.appendChild(this.mi("d")),h.appendChild(this.mi(f.value)),g.appendChild(h),d.appendChild(g),c.type!=e.OPERATOR||"+"!=c.value&&"-"!=c.value?d.appendChild(c.toMathML(a)):d.appendChild(this.addP(c,a));else if("diff"==b.value&&null!=this.children&&4==this.children.length)d=document.createElement("mrow"),g=document.createElement("mfrac"),b=document.createElement("msup"),b.appendChild(this.mi("d")),b.appendChild(l.toMathML(a)),g.appendChild(b),
h=document.createElement("mrow"),h.appendChild(this.mi("d")),b=document.createElement("msup"),b.appendChild(f.toMathML(a)),b.appendChild(l.toMathML(a)),h.appendChild(b),g.appendChild(h),d.appendChild(g),c.type!=e.OPERATOR||"+"!=c.value&&"-"!=c.value?d.appendChild(c.toMathML(a)):d.appendChild(this.addP(c,a));else if("integrate"==b.value&&null!=this.children&&3==this.children.length)d=document.createElement("mrow"),h=this.mo("\u222b"),h.setAttribute("stretchy","true"),d.appendChild(h),f.type!=e.OPERATOR||
"+"!=f.value&&"-"!=f.value?d.appendChild(c.toMathML(a)):d.appendChild(this.addP(c,a)),d.appendChild(this.mi("d")),d.appendChild(f.toMathML(a));else if("integrate"==b.value&&null!=this.children&&5==this.children.length)d=document.createElement("mrow"),b=document.createElement("msubsup"),h=this.mo("\u222b"),h.setAttribute("stretchy","true"),b.appendChild(h),b.appendChild(l.toMathML(a)),b.appendChild(g.toMathML(a)),d.appendChild(b),f.type!=e.OPERATOR||"+"!=f.value&&"-"!=f.value?d.appendChild(c.toMathML(a)):
d.appendChild(this.addP(c,a)),d.appendChild(this.mi("d")),d.appendChild(f.toMathML(a));else if("sum"==b.value&&null!=this.children&&5==this.children.length)d=document.createElement("mrow"),b=document.createElement("munderover"),h=this.mo("\u2211"),h.setAttribute("stretchy","true"),b.appendChild(h),h=document.createElement("mrow"),h.appendChild(f.toMathML(a)),h.appendChild(this.mo("=")),h.appendChild(l.toMathML(a)),b.appendChild(h),b.appendChild(g.toMathML(a)),d.appendChild(b),f.type!=e.OPERATOR||
"+"!=f.value&&"-"!=f.value?d.appendChild(c.toMathML(a)):d.appendChild(this.addP(c,a));else if("product"==b.value&&null!=this.children&&5==this.children.length)d=document.createElement("mrow"),b=document.createElement("munderover"),h=this.mo("\u220f"),h.setAttribute("stretchy","true"),b.appendChild(h),h=document.createElement("mrow"),h.appendChild(f.toMathML(a)),h.appendChild(this.mo("=")),h.appendChild(l.toMathML(a)),b.appendChild(h),b.appendChild(g.toMathML(a)),d.appendChild(b),f.type!=e.OPERATOR||
"+"!=f.value&&"-"!=f.value?d.appendChild(c.toMathML(a)):d.appendChild(this.addP(c,a));else if("limit"==b.value)d=document.createElement("mrow"),4>this.children.length?d.appendChild(this.mo("lim")):(b=document.createElement("munder"),b.appendChild(this.mo("lim")),h=document.createElement("mrow"),h.appendChild(f.toMathML(a)),h.appendChild(this.mo("\u2192")),h.appendChild(l.toMathML(a)),null!=g&&("plus"==g.value?h.appendChild(this.mo("+")):"minus"==g.value&&h.appendChild(this.mo("-"))),b.appendChild(h),
d.appendChild(b)),d.appendChild(c.toMathML(a));else{if("binomial"==b.value){d=document.createElement("mrow");d.appendChild(this.mo("("));l=document.createElement("mtable");for(c=1;c<this.children.length;c++)b=document.createElement("mtr"),b.appendChild(this.children[c].toMathML(a)),l.appendChild(b);d.appendChild(l)}else if("matrix"==b.value){for(c=1;c<this.children.length;c++)if(this.children[c].type!==e.VECTOR)return d=document.createElement("mtext"),d.appendChild(document.createTextNode("???")),
d;d=document.createElement("mrow");d.appendChild(this.mo("("));l=document.createElement("mtable");for(c=1;c<this.children.length;c++){b=document.createElement("mtr");for(f=0;f<this.children[c].children.length;f++)b.appendChild(this.children[c].children[f].toMathML(a));l.appendChild(b)}d.appendChild(l)}else for(d=document.createElement("mrow"),d.appendChild(b.toMathML(a)),d.appendChild(this.mo("(")),c=1;c<this.children.length;c++)d.appendChild(this.children[c].toMathML(a)),c<this.children.length-1&&
d.appendChild(this.mo(k.ARG_SEPARATOR));d.appendChild(this.mo(")"))}return d;case e.VECTOR:g=!0;for(c=0;c<this.children.length;c++)this.children[c].type!==e.VECTOR&&(g=!1);d=document.createElement("mrow");d.appendChild(this.mo("("));l=document.createElement("mtable");for(c=0;c<this.children.length;c++){b=document.createElement("mtr");if(g)for(f=0;f<this.children[c].children.length;f++)b.appendChild(this.children[c].children[f].toMathML(a));else b.appendChild(this.children[c].toMathML(a));l.appendChild(b)}d.appendChild(l);
d.appendChild(this.mo(")"));return d;case e.SUBSCRIPT:f=document.createElement("msub");f.appendChild(b.toMathML(a));if(2<this.children.length){d=document.createElement("mrow");for(c=1;c<this.children.length;c++)d.appendChild(this.children[c].toMathML(a)),c<this.children.length-1&&d.appendChild(this.mo(k.ARG_SEPARATOR));f.appendChild(d)}else f.appendChild(c.toMathML(a));return f}};e.prototype.mi=function(a){var b=document.createElement("mi");e.symbols[a]&&(a=e.symbols[a]);b.appendChild(document.createTextNode(a));
return b};e.prototype.mn=function(a){var b=document.createElement("mn");b.appendChild(document.createTextNode(a));return b};e.prototype.mo=function(a){var b=document.createElement("mo");e.symbols[a]&&(a=e.symbols[a]);b.appendChild(document.createTextNode(a));return b};e.prototype.addP=function(a,b){var c,f;c=document.createElement("mrow");f=this.mo("(");f.setAttribute("mathcolor",e.COLORS[b.depth%e.COLORS.length]);c.appendChild(f);b.depth++;c.appendChild(a.toMathML(b));b.depth--;f=this.mo(")");f.setAttribute("mathcolor",
e.COLORS[b.depth%e.COLORS.length]);c.appendChild(f);return c};e.symbols={alpha:"\u03b1",beta:"\u03b2",gamma:"\u03b3",delta:"\u03b4",epsilon:"\u03b5",zeta:"\u03b6",eta:"\u03b7",theta:"\u03b8",iota:"\u03b9",kappa:"\u03ba",lambda:"\u03bb",mu:"\u03bc",nu:"\u03bd",xi:"\u03be",omicron:"\u03bf",pi:"\u03c0",rho:"\u03c1",sigma:"\u03c3",tau:"\u03c4",upsilon:"\u03c5",phi:"\u03c6",chi:"\u03c7",psi:"\u03c8",omega:"\u03c9",Alpha:"\u0391",Beta:"\u0392",Gamma:"\u0393",Delta:"\u0394",Epsilon:"\u0395",Zeta:"\u0396",
Eta:"\u0397",Theta:"\u0398",Iota:"\u0399",Kappa:"\u039a",Lambda:"\u039b",Mu:"\u039c",Nu:"\u039d",Xi:"\u039e",Omicron:"\u039f",Pi:"\u03a0",Rho:"\u03a1",Sigma:"\u03a3",Tau:"\u03a4",Upsilon:"\u03a5",Phi:"\u03a6",Chi:"\u03a7",Psi:"\u03a8",Omega:"\u03a9","#":"\u2260",">=":"\u2265","<=":"\u2264",inf:"\u221e",minf:"-\u221e",hbar:"\u210f",G:"\ud835\udca2"};n.UNKNOWN=0;n.UNARY=1;n.BINARY=2;n.TERNARY=3;p.prototype.toString=function(){return this.msg+" at "+this.from+" - "+this.to};r.prototype.expression=function(a){var b,
c=this.current_token;if(null==c)throw new p("Expected something at the end",this.tokens[this.tokens.length-1].to+1);this.advance();if(null==c.op)b=new e(c.type,null,c.value,null);else{if(null==c.op.nud)throw new p("Unexpected operator '"+c.op.id+"'",c.from);b=c.op.nud(this)}for(;null!=this.current_token&&null!=this.current_token.op&&a<this.current_token.op.lbp;)c=this.current_token,this.advance(),b=c.op.led(this,b);return b};r.prototype.advance=function(a){if(a&&(null==this.current_token||null==this.current_token.op||
this.current_token.op.id!==a)){if(null==this.current_token)throw new p("Expected '"+a+"' at the end",this.tokens[this.tokens.length-1].to+1);throw new p("Expected '"+a+"'",this.current_token.from);}this.token_nr>=this.tokens.length?this.current_token=null:(this.current_token=this.tokens[this.token_nr],this.token_nr+=1)};r.prototype.addHiddenOperators=function(){for(var a=this.defs.findOperator("*"),b=this.defs.findOperator("`"),c=!1,f=!1,e=0;e<this.tokens.length-1;e++){var g=this.tokens[e],d=this.tokens[e+
1];this.unit_mode&&("`"==g.value?c=!0:c&&("^"==g.value?f=!0:f&&g.type==m.NUMBER?f=!1:f||g.type!=m.NUMBER?g.type==m.OPERATOR&&-1=="*/^()".indexOf(g.value)?c=!1:g.type==m.NAME&&"("==d.value&&(c=!1):c=!1));if(g.type==m.NAME&&d.type==m.NAME||g.type==m.NUMBER&&d.type==m.NAME||g.type==m.NUMBER&&d.type==m.NUMBER||g.type==m.NUMBER&&("("==d.value||"["==d.value)||(")"==g.value||"]"==g.value)&&d.type==m.NAME||(")"==g.value||"]"==g.value)&&d.type==m.NUMBER||(")"==g.value||"]"==g.value)&&"("==d.value){if(g=this.unit_mode&&
!c&&(g.type==m.NUMBER||")"==g.value||"]"==g.value)&&(d.type==m.NAME||("("==d.value||"["==d.value)&&this.tokens.length>e+2&&this.tokens[e+2].type==m.NAME)){var h,k;d.type==m.NAME?(h=d,k=e+1):(k=e+2,h=this.tokens[k]);for(var q=0;q<this.constants.length;q++)if(h.value==this.constants[q]){g=!1;break}if(this.tokens.length>k+1&&"("==this.tokens[k+1].value)for(k="pow sqrt abs exp factorial diff integrate sum product limit binomial matrix ln log log10 mod signum ceiling floor sin cos tan asin acos atan atan2 sinh cosh tanh asinh acosh atanh".split(" "),
q=0;q<k.length;q++)if(h.value==k[q]){g=!1;break}}g?(d=new m(m.OPERATOR,d.from,d.from,b.id,b),c=!0):d=new m(m.OPERATOR,d.from,d.from,a.id,a);this.tokens.splice(e+1,0,d)}}};r.prototype.parse=function(a){this.tokens=(new s(this.defs,a)).tokenize();if(0==this.tokens.length)return null;this.implicit_operators&&this.addHiddenOperators();this.token_nr=0;this.current_token=this.tokens[this.token_nr];this.advance();a=this.expression(0);if(null!=this.current_token)throw new p("Expected the end",this.current_token.from);
return a};s.prototype.tokenize=function(){var a,b,c,f,e;b=0;a=this.text.charAt(b);e=[];a:for(;a;)if(f=b," ">=a)b++,a=this.text.charAt(b);else{if("0"<=a&&"9">=a||(a===k.DECIMAL_SIGN_1||a===k.DECIMAL_SIGN_2)&&"0"<=this.text.charAt(b+1)&&"9">=this.text.charAt(b+1)){c="";if(a!==k.DECIMAL_SIGN_1&&a!==k.DECIMAL_SIGN_2)for(b++,c+=a;;){a=this.text.charAt(b);if("0">a||"9"<a)break;b++;c+=a}if(a===k.DECIMAL_SIGN_1||a===k.DECIMAL_SIGN_2)for(b++,c+=a;;){a=this.text.charAt(b);if("0">a||"9"<a)break;b+=1;c+=a}if("e"===
a||"E"===a){b++;c+=a;a=this.text.charAt(b);if("-"===a||"+"===a)b++,c+=a,a=this.text.charAt(b);if("0">a||"9"<a)throw new p("syntax error in number exponent",f,b);do b++,c+=a,a=this.text.charAt(b);while("0"<=a&&"9">=a)}var g=+c.replace(k.DECIMAL_SIGN_1,".").replace(k.DECIMAL_SIGN_2,".");if(isFinite(g)){e.push(new m(m.NUMBER,f,b-1,c,null));continue}else throw new p("syntax error in number",f,b);}for(c=0;c<this.defs.operators.length;c++)if(g=this.defs.operators[c],this.text.substring(b,b+g.id.length)===
g.id){b+=g.id.length;a=this.text.charAt(b);e.push(new m(m.OPERATOR,f,b-1,g.id,g));continue a}if("a"<=a&&"z">=a||"A"<=a&&"Z">=a){c=a;for(b++;;)if(a=this.text.charAt(b),"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"0"<=a&&"9">=a||"_"===a)c+=a,b++;else break;e.push(new m(m.NAME,f,b-1,c,null))}else throw new p("unrecognized operator",f,b);}return e};m.UNKNOWN=0;m.NAME=1;m.NUMBER=2;m.OPERATOR=3;"use strict";var t=!1;return{Definitions:k,ENode:e,Operator:n,ParseException:p,Parser:r,initEditors:function(){if(!t){t=!0;
for(var a=[],b=document.getElementsByClassName("math"),c=0;c<b.length;c++){var f=b[c];if("TEXTAREA"==f.nodeName||"INPUT"==f.nodeName){var e=document.createElement("span");e.setAttribute("style","display:none");f.nextSibling?f.parentNode.insertBefore(e,f.nextSibling):f.parentNode.appendChild(e);f.addEventListener("blur",function(a){return function(b){a.setAttribute("style","display:none")}}(e),!1);f.addEventListener("focus",function(a){return function(b){a.setAttribute("style","display: inline-block; background-color: #FFFFE0")}}(e),
!1);var g="true"===f.getAttribute("data-implicit_operators"),d="true"===f.getAttribute("data-unit_mode"),h=f.getAttribute("data-constants");h&&(h=h.split(/[\s,]+/));a[c]={ta:f,output_node:e,oldtxt:"",parser:new r(g,d,h)};e=function(b){return function(c){var d=a[b];document.activeElement==d.ta&&d.output_node.setAttribute("style","display: inline-block; background-color: #FFFFE0");var e,f,g,h;e=d.ta;c=d.output_node;e=e.value;g="";for(h=e;h!=g;)g=h,h=g.replace(/\[[^\[\]]*\]/g,"");if(h.split("[").length==
h.split("]").length){for(g="";h!=g;)g=h,h=g.replace(/\([^\(\)]*\)/g,"");h.split("(").length==h.split(")").length&&-1!=h.indexOf(k.ARG_SEPARATOR)&&(e="["+e+"]")}if(e!=d.oldtxt){for(d.oldtxt=e;null!=c.firstChild;)c.removeChild(c.firstChild);c.removeAttribute("title");if(""!=e){d=d.parser;try{if(f=d.parse(e),null!=f){var m=document.createElement("math");m.setAttribute("display","block");m.appendChild(f.toMathML());c.appendChild(m);MathJax.Hub.Queue(["Typeset",MathJax.Hub,c])}}catch(l){f="error: "+l,
c.setAttribute("title",f),l instanceof p?(c.appendChild(document.createTextNode(e.substring(0,l.from))),f=document.createElement("span"),f.appendChild(document.createTextNode(e.substring(l.from,l.to+1))),f.className="math-error",c.appendChild(f),l.to<e.length-1&&c.appendChild(document.createTextNode(e.substring(l.to+1)))):(e=document.createTextNode(f),c.appendChild(e))}}}}}(c);""!=f.value&&e();f.addEventListener("change",e,!1);f.addEventListener("keyup",e,!1)}}}}}}();
Index: loncom/html/adm/LC_math_editor/src/definitions.js
+++ loncom/html/adm/LC_math_editor/src/definitions.js
/*

Copyright (C) 2014  Michigan State University Board of Trustees

The JavaScript code in this page is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version.  The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.

As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.

*/

/**
 * Operator definitions (see function define() at the end).
 * @constructor
 */
function Definitions() {
    this.operators = [];  /* Array of Operator */
}

Definitions.ARG_SEPARATOR = ";";
Definitions.DECIMAL_SIGN_1 = ".";
Definitions.DECIMAL_SIGN_2 = ",";

/**
 * Creates a new operator.
 * @param {string} id - Operator id (text used to recognize it)
 * @param {number} arity - Operator.UNARY, BINARY or TERNARY
 * @param {number} lbp - Left binding power
 * @param {number} rbp - Right binding power
 * @param {function} nud - Null denotation function
 * @param {function} led - Left denotation function
 */
Definitions.prototype.operator = function(id, arity, lbp, rbp, nud, led) {
    this.operators.push(new Operator(id, arity, lbp, rbp, nud, led));
};

/**
 * Creates a new separator operator.
 * @param {string} id - Operator id (text used to recognize it)
 */
Definitions.prototype.separator = function(id) {
    this.operator(id, Operator.BINARY, 0, 0, null, null);
};

/**
 * Creates a new infix operator.
 * @param {string} id - Operator id (text used to recognize it)
 * @param {number} lbp - Left binding power
 * @param {number} rbp - Right binding power
 * @param {ledFunction} [led] - Left denotation function
 */
Definitions.prototype.infix = function(id, lbp, rbp, led) {
    var arity, nud;
    arity = Operator.BINARY;
    nud = null;
    led = led || function(p, left) {
        var children = [left, p.expression(rbp)];
        return new ENode(ENode.OPERATOR, this, id, children);
    };
    this.operator(id, arity, lbp, rbp, nud, led);
};

/**
 * Creates a new prefix operator.
 * @param {string} id - Operator id (text used to recognize it)
 * @param {number} rbp - Right binding power
 * @param {nudFunction} [nud] - Null denotation function
 */
Definitions.prototype.prefix = function(id, rbp, nud) {
    var arity, lbp, led;
    arity = Operator.UNARY;
    lbp = 0;
    nud = nud || function(p) {
        var children = [p.expression(rbp)];
        return new ENode(ENode.OPERATOR, this, id, children);
    };
    led = null;
    this.operator(id, arity, lbp, rbp, nud, led);
};

/**
 * Creates a new suffix operator.
 * @param {string} id - Operator id (text used to recognize it)
 * @param {number} lbp - Left binding power
 * @param {ledFunction} [led] - Left denotation function
 */
Definitions.prototype.suffix = function(id, lbp, led) {
    var arity, rbp, nud;
    arity = Operator.UNARY;
    rbp = 0;
    nud = null;
    led = led || function(p, left) {
        var children = [left];
        return new ENode(ENode.OPERATOR, this, id, children);
    };
    this.operator(id, arity, lbp, rbp, nud, led);
};

/**
 * Returns the defined operator with the given id
 * @param {string} id - Operator id (text used to recognize it)
 * @returns {Operator}
 */
Definitions.prototype.findOperator = function(id) {
    for (var i=0; i<this.operators.length; i++) {
        if (this.operators[i].id == id) {
            return(this.operators[i]);
        }
    }
    return null;
}

/**
 * Defines all the operators.
 */
Definitions.prototype.define = function() {
    this.suffix("!", 160);
    this.infix("^", 140, 139);
    this.infix(".", 130, 129);
    this.infix("`", 125, 125, function(p, left) {
        // led (infix operator)
        // this led for units gathers all the units in an ENode
        var right = p.expression(125);
        while (p.current_token != null && "*/".indexOf(p.current_token.value) != -1) {
            var token2 = p.tokens[p.token_nr];
            if (token2 == null)
                break;
            if (token2.type != Token.NAME && token2.value != "(")
                break;
            var token3 = p.tokens[p.token_nr+1];
            if (token3 != null && (token3.value == "(" || token3.type == Token.NUMBER))
                break;
            if (p.unit_mode && p.tokens[p.token_nr].type == Token.NAME) {
                var nv = p.tokens[p.token_nr].value;
                var cst = false;
                for (var i=0; i<p.constants.length; i++) {
                    if (nv == p.constants[i]) {
                        cst = true;
                        break;
                    }
                }
                if (cst)
                    break;
            }
            var t = p.current_token;
            p.advance();
            right = t.op.led(p, right);
        }
        var children = [left, right];
        return new ENode(ENode.OPERATOR, this, "`", children);
    });
    this.infix("*", 120, 120);
    this.infix("/", 120, 120);
    this.infix("%", 120, 120);
    this.infix("+", 100, 100);
    this.operator("-", Operator.BINARY, 100, 134, function(p) {
        // nud (prefix operator)
        var children = [p.expression(134)];
        return new ENode(ENode.OPERATOR, this, "-", children);
    }, function(p, left) {
        // led (infix operator)
        var children = [left, p.expression(100)];
        return new ENode(ENode.OPERATOR, this, "-", children);
    });
    this.infix("=", 80, 80);
    this.infix("#", 80, 80);
    this.infix("<=", 80, 80);
    this.infix(">=", 80, 80);
    this.infix("<", 80, 80);
    this.infix(">", 80, 80);
    
    this.separator(")");
    this.separator(Definitions.ARG_SEPARATOR);
    this.operator("(", Operator.BINARY, 200, 200, function(p) {
        // nud (for parenthesis)
        var e = p.expression(0);
        p.advance(")");
        return e;
    }, function(p, left) {
        // led (for functions)
        if (left.type != ENode.NAME && left.type != ENode.SUBSCRIPT)
            throw new ParseException("Function name expected before a parenthesis.", p.tokens[p.token_nr - 1].from);
        var children = [left];
        if (p.current_token == null || p.current_token.op == null || p.current_token.op.id !== ")") {
            while (true) {
                children.push(p.expression(0));
                if (p.current_token == null || p.current_token.op == null || p.current_token.op.id !== Definitions.ARG_SEPARATOR) {
                    break;
                }
                p.advance(Definitions.ARG_SEPARATOR);
            }
        }
        p.advance(")");
        return new ENode(ENode.FUNCTION, this, "(", children);
    });
    
    this.separator("]");
    this.operator("[", Operator.BINARY, 200, 70, function(p) {
        // nud (for vectors)
        var children = [];
        if (p.current_token == null || p.current_token.op == null || p.current_token.op.id !== "]") {
            while (true) {
                children.push(p.expression(0));
                if (p.current_token == null || p.current_token.op == null || p.current_token.op.id !== Definitions.ARG_SEPARATOR) {
                    break;
                }
                p.advance(Definitions.ARG_SEPARATOR);
            }
        }
        p.advance("]");
        return new ENode(ENode.VECTOR, this, null, children);
    }, function(p, left) {
        // led (for subscript)
        if (left.type != ENode.NAME && left.type != ENode.SUBSCRIPT)
            throw new ParseException("Name expected before a square bracket.", p.tokens[p.token_nr - 1].from);
        var children = [left];
        if (p.current_token == null || p.current_token.op == null || p.current_token.op.id !== "]") {
            while (true) {
                children.push(p.expression(0));
                if (p.current_token == null || p.current_token.op == null || p.current_token.op.id !== Definitions.ARG_SEPARATOR) {
                    break;
                }
                p.advance(Definitions.ARG_SEPARATOR);
            }
        }
        p.advance("]");
        return new ENode(ENode.SUBSCRIPT, this, "[", children);
    });
};


Index: loncom/html/adm/LC_math_editor/src/enode.js
+++ loncom/html/adm/LC_math_editor/src/enode.js
/*

Copyright (C) 2014  Michigan State University Board of Trustees

The JavaScript code in this page is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version.  The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.

As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.

*/

/**
 * Parsed tree node. ENode.toMathML(hcolors) contains the code for the transformation into MathML.
 * @constructor
 * @param {number} type - ENode.UNKNOWN | NAME | NUMBER | OPERATOR | FUNCTION | VECTOR
 * @param {Operator} op - The operator
 * @param {string} value - Node value as a string, null for type VECTOR
 * @param {Array.<ENode>} children - The children nodes, only for types OPERATOR, FUNCTION, VECTOR, SUBSCRIPT
 */
function ENode(type, op, value, children) {
    this.type = type;
    this.op = op;
    this.value = value;
    this.children = children;
}

ENode.UNKNOWN = 0;
ENode.NAME = 1;
ENode.NUMBER = 2;
ENode.OPERATOR = 3;
ENode.FUNCTION = 4;
ENode.VECTOR = 5;
ENode.SUBSCRIPT = 6;
ENode.COLORS = ["#E01010", "#0010FF", "#009000", "#FF00FF", "#00B0B0", "#F09000", 
                "#800080", "#F080A0", "#6090F0", "#902000", "#70A050", "#A07060",
                "#5000FF", "#E06050", "#008080", "#808000"];

/**
 * Returns the node as a string, for debug
 * @returns {string}
 */
ENode.prototype.toString = function() {
    var s = '(';
    switch (this.type) {
        case ENode.UNKNOWN:
            s += 'UNKNOWN';
            break;
        case ENode.NAME:
            s += 'NAME';
            break;
        case ENode.NUMBER:
            s += 'NUMBER';
            break;
        case ENode.OPERATOR:
            s += 'OPERATOR';
            break;
        case ENode.FUNCTION:
            s += 'FUNCTION';
            break;
        case ENode.VECTOR:
            s += 'VECTOR';
            break;
        case ENode.SUBSCRIPT:
            s += 'SUBSCRIPT';
            break;
    }
    if (this.op)
        s += " '" + this.op.id + "'";
    if (this.value)
        s += " '" + this.value + "'";
    if (this.children) {
        s += ' [';
        for (var i = 0; i < this.children.length; i++) {
            s += this.children[i].toString();
            if (i != this.children.length - 1)
                s += ',';
        }
        s += ']';
    }
    s+= ')';
    return s;
};

/**
 * Returns the color for an identifier.
 * @param {string} name
 * @param {Object.<string, string>} hcolors - hash identifier->color
 * @returns {string}
 */
ENode.prototype.getColorForIdentifier = function(name, hcolors) {
    var res = hcolors[name];
    if (!res) {
        res = ENode.COLORS[Object.keys(hcolors).length % ENode.COLORS.length];
        hcolors[name] = res;
    }
    return res;
}

/**
 * Transforms this ENode into a MathML HTML DOM element.
 * @param {Object} [context] - display context (not needed for the root element)
 * @param {Object.<string, string>} context.hcolors - hash identifier->color
 * @param {number} context.depth - Depth in parenthesis, used for coloring
 * @returns {Element}
 */
ENode.prototype.toMathML = function(context) {
    var c0, c1, c2, c3, c4, i, j, el, par, mrow, mo, mtable, mfrac, msub, msup;
    if (typeof context == "undefined")
        context = { hcolors: {}, depth: 0 };
    if (this.children != null && this.children.length > 0)
        c0 = this.children[0];
    else
        c0 = null;
    if (this.children != null && this.children.length > 1)
        c1 = this.children[1];
    else
        c1 = null;
    if (this.children != null && this.children.length > 2)
        c2 = this.children[2];
    else
        c2 = null;
    if (this.children != null && this.children.length > 3)
        c3 = this.children[3];
    else
        c3 = null;
    if (this.children != null && this.children.length > 4)
        c4 = this.children[4];
    else
        c4 = null;
    
    switch (this.type) {
        case ENode.UNKNOWN:
            el = document.createElement('mtext');
            el.appendChild(document.createTextNode("???"));
            return(el);
        
        case ENode.NAME:
            if (this.value.search(/^[a-zA-Z]+[0-9]+$/) >= 0) {
                var ind = this.value.search(/[0-9]/);
                msub = document.createElement('msub');
                msub.appendChild(this.mi(this.value.substring(0,ind)));
                msub.appendChild(this.mn(this.value.substring(ind)));
                el = msub;
            } else {
                el = this.mi(this.value)
            }
            el.setAttribute("mathcolor", this.getColorForIdentifier(this.value, context.hcolors));
            return(el);
        
        case ENode.NUMBER:
            if (this.value.indexOf('e') != -1 || this.value.indexOf('E') != -1) {
                var index = this.value.indexOf('e');
                if (index == -1)
                    index = this.value.indexOf('E');
                mrow = document.createElement('mrow');
                mrow.appendChild(this.mn(this.value.substring(0, index)));
                mrow.appendChild(this.mo("\u22C5"));
                msup = document.createElement('msup');
                msup.appendChild(this.mn(10));
                msup.appendChild(this.mn(this.value.substring(index + 1)));
                mrow.appendChild(msup);
                return(mrow);
            }
            return(this.mn(this.value));
        
        case ENode.OPERATOR:
            if (this.value == "/") {
                mfrac = document.createElement('mfrac');
                mfrac.appendChild(c0.toMathML(context));
                mfrac.appendChild(c1.toMathML(context));
                el = mfrac;
            } else if (this.value == "^") {
                if (c0.type == ENode.FUNCTION) {
                    if (c0.value == "sqrt" || c0.value == "abs" || c0.value == "matrix" ||
                            c0.value == "diff")
                        par = false;
                    else
                        par = true;
                } else if (c0.type == ENode.OPERATOR) {
                    par = true;
                } else
                    par = false;
                el = document.createElement('msup');
                if (par)
                    el.appendChild(this.addP(c0, context));
                else
                    el.appendChild(c0.toMathML(context));
                el.appendChild(c1.toMathML(context));
            } else if (this.value == "*") {
                mrow = document.createElement('mrow');
                if (c0.type == ENode.OPERATOR && (c0.value == "+" || c0.value == "-"))
                    mrow.appendChild(this.addP(c0, context));
                else
                    mrow.appendChild(c0.toMathML(context));
                // should the x operator be visible ? We need to check if there is a number to the left of c1
                var firstinc1 = c1;
                while (firstinc1.type == ENode.OPERATOR) {
                    firstinc1 = firstinc1.children[0];
                }
                // ... and if it's an operation between vectors/matrices, the * operator should be displayed
                // (it is ambiguous otherwise)
                // note: this will not work if the matrix is calculated, for instance with 2[1;2]*[3;4]
                if (c0.type == ENode.VECTOR && c1.type == ENode.VECTOR)
                    mrow.appendChild(this.mo("*"));
                else if (firstinc1.type == ENode.NUMBER)
                    mrow.appendChild(this.mo("\u22C5"));
                if (c1.type == ENode.OPERATOR && (c1.value == "+" || c1.value == "-"))
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                el = mrow;
            } else if (this.value == "-") {
                mrow = document.createElement('mrow');
                if (this.children.length == 1) {
                    mrow.appendChild(this.mo("-"));
                    mrow.appendChild(c0.toMathML(context));
                } else {
                    mrow.appendChild(c0.toMathML(context));
                    mrow.appendChild(this.mo("-"));
                    if (c1.type == ENode.OPERATOR && (c1.value == "+" || c1.value == "-"))
                        mrow.appendChild(this.addP(c1, context));
                    else
                        mrow.appendChild(c1.toMathML(context));
                }
                el = mrow;
            } else if (this.value == "!") {
                mrow = document.createElement('mrow');
                mo = this.mo(this.value);
                if (c0.type == ENode.OPERATOR && (c0.value == "+" || c0.value == "-"))
                    mrow.appendChild(this.addP(c0, context));
                else
                    mrow.appendChild(c0.toMathML(context));
                mrow.appendChild(mo);
                el = mrow;
            } else if (this.value == "+") {
                mrow = document.createElement('mrow');
                mo = this.mo(this.value);
                mrow.appendChild(c0.toMathML(context));
                mrow.appendChild(mo);
                // should we add parenthesis ? We need to check if there is a '-' to the left of c1
                par = false;
                var first = c1;
                while (first.type == ENode.OPERATOR) {
                    if (first.value == "-" && first.children.length == 1) {
                        par = true;
                        break;
                    } else if (first.value == "+" || first.value == "-" || first.value == "*") {
                        first = first.children[0];
                    } else {
                        break;
                    }
                }
                if (par)
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                el = mrow;
            } else if (this.value == ".") {
                mrow = document.createElement('mrow');
                if (c0.type == ENode.OPERATOR && (c0.value == "+" || c0.value == "-"))
                    mrow.appendChild(this.addP(c0, context));
                else
                    mrow.appendChild(c0.toMathML(context));
                mrow.appendChild(this.mo("\u22C5"));
                if (c1.type == ENode.OPERATOR && (c1.value == "+" || c1.value == "-"))
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                el = mrow;
            } else if (this.value == "`") {
                mrow = document.createElement('mrow');
                if (c0.type == ENode.OPERATOR && (c0.value == "+" || c0.value == "-"))
                    mrow.appendChild(this.addP(c0, context));
                else
                    mrow.appendChild(c0.toMathML(context));
                // the units should not be in italics
                var mstyle = document.createElement("mstyle");
                mstyle.setAttribute("fontstyle", "normal");
                if (c1.type == ENode.OPERATOR && (c1.value == "+" || c1.value == "-"))
                    mstyle.appendChild(this.addP(c1, context));
                else
                    mstyle.appendChild(c1.toMathML(context));
                mrow.appendChild(mstyle);
                el = mrow;
            } else {
                // relational operators
                mrow = document.createElement('mrow');
                mo = this.mo(this.value);
                mrow.appendChild(c0.toMathML(context));
                mrow.appendChild(mo);
                mrow.appendChild(c1.toMathML(context));
                el = mrow;
            }
            return(el);
        
        case ENode.FUNCTION: /* TODO: throw exceptions if wrong nb of args ? */
            // c0 contains the function name
            if (c0.value == "sqrt" && c1 != null) {
                el = document.createElement('msqrt');
                el.appendChild(c1.toMathML(context));
            } else if (c0.value == "abs" && c1 != null) {
                mrow = document.createElement('mrow');
                mrow.appendChild(this.mo("|"));
                mrow.appendChild(c1.toMathML(context));
                mrow.appendChild(this.mo("|"));
                el = mrow;
            } else if (c0.value == "exp" && c1 != null) {
                el = document.createElement('msup');
                el.appendChild(this.mi("e"));
                el.appendChild(c1.toMathML(context));
            } else if (c0.value == "factorial") {
                mrow = document.createElement('mrow');
                mo = this.mo("!");
                if (c1.type == ENode.OPERATOR && (c1.value == "+" || c1.value == "-"))
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                mrow.appendChild(mo);
                el = mrow;
            } else if (c0.value == "diff" && this.children != null && this.children.length == 3) {
                mrow = document.createElement('mrow');
                mfrac = document.createElement('mfrac');
                mfrac.appendChild(this.mi("d"));
                var f2 = document.createElement('mrow');
                f2.appendChild(this.mi("d"));
                f2.appendChild(this.mi(c2.value));
                mfrac.appendChild(f2);
                mrow.appendChild(mfrac);
                if (c1.type == ENode.OPERATOR && (c1.value == "+" || c1.value == "-"))
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                el = mrow;
            } else if (c0.value == "diff" && this.children != null && this.children.length == 4) {
                mrow = document.createElement('mrow');
                mfrac = document.createElement('mfrac');
                msup = document.createElement('msup');
                msup.appendChild(this.mi("d"));
                msup.appendChild(c3.toMathML(context));
                mfrac.appendChild(msup);
                var f2 = document.createElement('mrow');
                f2.appendChild(this.mi("d"));
                msup = document.createElement('msup');
                msup.appendChild(c2.toMathML(context));
                msup.appendChild(c3.toMathML(context));
                f2.appendChild(msup);
                mfrac.appendChild(f2);
                mrow.appendChild(mfrac);
                if (c1.type == ENode.OPERATOR && (c1.value == "+" || c1.value == "-"))
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                el = mrow;
            } else if (c0.value == "integrate" && this.children != null && this.children.length == 3) {
                mrow = document.createElement('mrow');
                var mo = this.mo("\u222B");
                mo.setAttribute("stretchy", "true"); // doesn't work with MathJax
                mrow.appendChild(mo);
                if (c2.type == ENode.OPERATOR && (c2.value == "+" || c2.value == "-"))
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                mrow.appendChild(this.mi("d"));
                mrow.appendChild(c2.toMathML(context));
                el = mrow;
            } else if (c0.value == "integrate" && this.children != null && this.children.length == 5) {
                mrow = document.createElement('mrow');
                var msubsup = document.createElement('msubsup');
                var mo = this.mo("\u222B");
                mo.setAttribute("stretchy", "true"); // doesn't work with MathJax
                msubsup.appendChild(mo);
                msubsup.appendChild(c3.toMathML(context));
                msubsup.appendChild(c4.toMathML(context));
                mrow.appendChild(msubsup);
                if (c2.type == ENode.OPERATOR && (c2.value == "+" || c2.value == "-"))
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                mrow.appendChild(this.mi("d"));
                mrow.appendChild(c2.toMathML(context));
                el = mrow;
            } else if (c0.value == "sum" && this.children != null && this.children.length == 5) {
                mrow = document.createElement('mrow');
                var munderover = document.createElement('munderover');
                var mo = this.mo("\u2211");
                mo.setAttribute("stretchy", "true"); // doesn't work with MathJax
                munderover.appendChild(mo);
                var mrow2 = document.createElement('mrow');
                mrow2.appendChild(c2.toMathML(context));
                mrow2.appendChild(this.mo("="));
                mrow2.appendChild(c3.toMathML(context));
                munderover.appendChild(mrow2);
                munderover.appendChild(c4.toMathML(context));
                mrow.appendChild(munderover);
                if (c2.type == ENode.OPERATOR && (c2.value == "+" || c2.value == "-"))
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                el = mrow;
            } else if (c0.value == "product" && this.children != null && this.children.length == 5) {
                mrow = document.createElement('mrow');
                var munderover = document.createElement('munderover');
                var mo = this.mo("\u220F");
                mo.setAttribute("stretchy", "true"); // doesn't work with MathJax
                munderover.appendChild(mo);
                var mrow2 = document.createElement('mrow');
                mrow2.appendChild(c2.toMathML(context));
                mrow2.appendChild(this.mo("="));
                mrow2.appendChild(c3.toMathML(context));
                munderover.appendChild(mrow2);
                munderover.appendChild(c4.toMathML(context));
                mrow.appendChild(munderover);
                if (c2.type == ENode.OPERATOR && (c2.value == "+" || c2.value == "-"))
                    mrow.appendChild(this.addP(c1, context));
                else
                    mrow.appendChild(c1.toMathML(context));
                el = mrow;
            } else if (c0.value == "limit") {
                mrow = document.createElement('mrow');
                if (this.children.length < 4) {
                    mrow.appendChild(this.mo("lim"));
                } else {
                    var munder = document.createElement('munder');
                    munder.appendChild(this.mo("lim"));
                    var mrowunder = document.createElement('mrow');
                    mrowunder.appendChild(c2.toMathML(context));
                    mrowunder.appendChild(this.mo("\u2192"));
                    mrowunder.appendChild(c3.toMathML(context));
                    if (c4 != null) {
                        if (c4.value == "plus")
                            mrowunder.appendChild(this.mo("+"));
                        else if (c4.value == "minus")
                            mrowunder.appendChild(this.mo("-"));
                    }
                    munder.appendChild(mrowunder);
                    mrow.appendChild(munder);
                }
                mrow.appendChild(c1.toMathML(context));
                el = mrow;
            } else if (c0.value == "binomial") {
                // displayed like a vector
                mrow = document.createElement('mrow');
                mrow.appendChild(this.mo("("));
                mtable = document.createElement('mtable');
                for (i=1; i<this.children.length; i++) {
                    var mtr = document.createElement('mtr');
                    mtr.appendChild(this.children[i].toMathML(context));
                    mtable.appendChild(mtr);
                }
                mrow.appendChild(mtable);
                mrow.appendChild(this.mo(")"));
                el = mrow;
            } else if (c0.value == "matrix") {
                for (i=1; i<this.children.length; i++) {
                    // check that all children are vectors
                    if (this.children[i].type !== ENode.VECTOR) {
                        el = document.createElement('mtext');
                        el.appendChild(document.createTextNode("???")); // could throw here
                        return(el);
                    }
                }
                mrow = document.createElement('mrow');
                mrow.appendChild(this.mo("("));
                mtable = document.createElement('mtable');
                for (i=1; i<this.children.length; i++) {
                    var mtr = document.createElement('mtr');
                    for (j=0; j<this.children[i].children.length; j++) {
                        mtr.appendChild(this.children[i].children[j].toMathML(context));
                    }
                    mtable.appendChild(mtr);
                }
                mrow.appendChild(mtable);
                mrow.appendChild(this.mo(")"));
                el = mrow;
            } else {
                // default display for a function
                mrow = document.createElement('mrow');
                mrow.appendChild(c0.toMathML(context));
                mrow.appendChild(this.mo("("));
                for (i=1; i<this.children.length; i++) {
                    mrow.appendChild(this.children[i].toMathML(context));
                    if (i < this.children.length - 1)
                        mrow.appendChild(this.mo(Definitions.ARG_SEPARATOR));
                }
                mrow.appendChild(this.mo(")"));
                el = mrow;
            }
            return(el);
        
        case ENode.VECTOR:
            var is_matrix = true;
            for (i=0; i<this.children.length; i++) {
                if (this.children[i].type !== ENode.VECTOR)
                    is_matrix = false;
            }
            mrow = document.createElement('mrow');
            mrow.appendChild(this.mo("("));
            mtable = document.createElement('mtable');
            for (i=0; i<this.children.length; i++) {
                var mtr = document.createElement('mtr');
                if (is_matrix) {
                    for (j=0; j<this.children[i].children.length; j++) {
                        mtr.appendChild(this.children[i].children[j].toMathML(context));
                    }
                } else {
                    mtr.appendChild(this.children[i].toMathML(context));
                }
                mtable.appendChild(mtr);
            }
            mrow.appendChild(mtable);
            mrow.appendChild(this.mo(")"));
            return(mrow);
            
        case ENode.SUBSCRIPT:
            msub = document.createElement('msub');
            msub.appendChild(c0.toMathML(context));
            if (this.children.length > 2) {
                mrow = document.createElement('mrow');
                for (i=1; i<this.children.length; i++) {
                    mrow.appendChild(this.children[i].toMathML(context));
                    if (i < this.children.length - 1)
                        mrow.appendChild(this.mo(Definitions.ARG_SEPARATOR));
                }
                msub.appendChild(mrow);
            } else {
                msub.appendChild(c1.toMathML(context));
            }
            return(msub);
    }
};

/**
 * Creates a MathML mi element with the given name
 * @param {string} name
 * @returns {Element}
 */
ENode.prototype.mi = function(name) {
    var mi = document.createElement('mi');
    if (ENode.symbols[name])
        name = ENode.symbols[name];
    mi.appendChild(document.createTextNode(name));
    return mi;
};

/**
 * Creates a MathML mn element with the given number or string
 * @param {string} n
 * @returns {Element}
 */
ENode.prototype.mn = function(n) {
    var mn = document.createElement('mn');
    mn.appendChild(document.createTextNode(n));
    return mn;
};

/**
 * Creates a MathML mo element with the given name
 * @param {string} name
 * @returns {Element}
 */
ENode.prototype.mo = function(name) {
    var mo = document.createElement('mo');
    if (ENode.symbols[name])
        name = ENode.symbols[name];
    mo.appendChild(document.createTextNode(name));
    return mo;
};

/**
 * Add parenthesis and returns a MathML element
 * @param {ENode} en
 * @param {Object} [context] - display context (not needed for the root element)
 * @param {Object.<string, string>} context.hcolors - hash identifier->color
 * @param {number} context.depth - Depth in parenthesis, used for coloring
 * @returns {Element}
 */
ENode.prototype.addP = function(en, context) {
    var mrow, mo;
    mrow = document.createElement('mrow');
    mo = this.mo("(");
    mo.setAttribute("mathcolor", ENode.COLORS[context.depth % ENode.COLORS.length]);
    mrow.appendChild(mo);
    context.depth++;
    mrow.appendChild(en.toMathML(context));
    context.depth--;
    mo = this.mo(")");
    mo.setAttribute("mathcolor", ENode.COLORS[context.depth % ENode.COLORS.length]);
    mrow.appendChild(mo);
    return mrow;
};

ENode.symbols = {
    /* lowercase greek */
    "alpha": "\u03B1", "beta": "\u03B2", "gamma": "\u03B3",
    "delta": "\u03B4", "epsilon": "\u03B5", "zeta": "\u03B6",
    "eta": "\u03B7", "theta": "\u03B8", "iota": "\u03B9",
    "kappa": "\u03BA", "lambda": "\u03BB", "mu": "\u03BC",
    "nu": "\u03BD", "xi": "\u03BE", "omicron": "\u03BF",
    "pi": "\u03C0", "rho": "\u03C1", "sigma": "\u03C3",
    "tau": "\u03C4", "upsilon": "\u03C5", "phi": "\u03C6",
    "chi": "\u03C7", "psi": "\u03C8", "omega": "\u03C9",
    /* uppercase greek */
    "Alpha": "\u0391", "Beta": "\u0392", "Gamma": "\u0393",
    "Delta": "\u0394", "Epsilon": "\u0395", "Zeta": "\u0396",
    "Eta": "\u0397", "Theta": "\u0398", "Iota": "\u0399",
    "Kappa": "\u039A", "Lambda": "\u039B", "Mu": "\u039C",
    "Nu": "\u039D", "Xi": "\u039E", "Omicron": "\u039F",
    "Pi": "\u03A0", "Rho": "\u03A1", "Sigma": "\u03A3",
    "Tau": "\u03A4", "Upsilon": "\u03A5", "Phi": "\u03A6",
    "Chi": "\u03A7", "Psi": "\u03A8", "Omega": "\u03A9",
    
    /* operators */
    "#":  "\u2260",
    ">=": "\u2265",
    "<=": "\u2264",
    
    /* other */
    "inf":  "\u221E",
    "minf": "-\u221E",
    "hbar": "\u210F",
    "G":    "\uD835\uDCA2" // 1D4A2
};

Index: loncom/html/adm/LC_math_editor/src/operator.js
+++ loncom/html/adm/LC_math_editor/src/operator.js
/*

Copyright (C) 2014  Michigan State University Board of Trustees

The JavaScript code in this page is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version.  The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.

As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.

*/

/**
 * Null denotation function
 * @callback nudFunction
 * @param {Parser} p - the parser
 * @returns {ENode}
 */

/**
 * Left denotation function
 * @callback ledFunction
 * @param {Parser} p - the parser
 * @param {ENode} left - left node
 * @returns {ENode}
 */

/**
 * Parser operator, like "(".
 * @constructor
 * @param {string} id - Characters used to recognize the operator
 * @param {number} arity (UNKNOWN, UNARY, BINARY, TERNARY)
 * @param {number} lbp - left binding power
 * @param {number} rbp - right binding power
 * @param {nudFunction} nud - Null denotation function
 * @param {ledFunction} led - Left denotation function
 */
function Operator(id, arity, lbp, rbp, nud, led) {
    this.id = id;
    this.arity = arity;
    this.lbp = lbp;
    this.rbp = rbp;
    this.nud = nud;
    this.led = led;
}

Operator.UNKNOWN = 0;
Operator.UNARY = 1;
Operator.BINARY = 2;
Operator.TERNARY = 3;

Index: loncom/html/adm/LC_math_editor/src/parse_exception.js
+++ loncom/html/adm/LC_math_editor/src/parse_exception.js
/*

Copyright (C) 2014  Michigan State University Board of Trustees

The JavaScript code in this page is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version.  The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.

As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.

*/

/**
 * Parse exception
 * @constructor
 * @param {string} msg - Error message
 * @param {number} from - Character index
 * @param {number} [to] - Character index to (inclusive)
 */
function ParseException(msg, from, to) {
    this.msg = msg;
    this.from = from;
    if (to)
        this.to = to;
    else
        this.to = this.from;
}

/**
 * Returns the exception as a string, for debug
 * @returns {string}
 */
ParseException.prototype.toString = function() {
    return(this.msg + " at " + this.from + " - " + this.to);
};

Index: loncom/html/adm/LC_math_editor/src/parser.js
+++ loncom/html/adm/LC_math_editor/src/parser.js
/*

Copyright (C) 2014  Michigan State University Board of Trustees

The JavaScript code in this page is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version.  The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.

As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.

*/

/**
 * Equation parser
 * @constructor
 * @param {boolean} [implicit_operators] - assume hidden multiplication and unit operators in some cases (unlike maxima)
 * @param {boolean} [unit_mode] - handle only numerical expressions with units (no variable)
 * @param {Array.<string>} [constants] - array of constant names for unit mode
 */
function Parser(implicit_operators, unit_mode, constants) {
    if (typeof implicit_operators == "undefined")
        this.implicit_operators = false;
    else
        this.implicit_operators = implicit_operators;
    if (typeof unit_mode == "undefined")
        this.unit_mode = false;
    else
        this.unit_mode = unit_mode;
    if (typeof constants == "undefined")
        this.constants = [];
    else
        this.constants = constants;
    this.defs = new Definitions();
    this.defs.define();
    this.operators = this.defs.operators;
    this.oph = {}; // operator hash table
    for (var i=0; i<this.operators.length; i++)
        this.oph[this.operators[i].id] = this.operators[i];
}

/**
 * Returns the right node at the current token, based on top-down operator precedence.
 * @param {number} rbp - Right binding power
 * @returns {ENode}
 */
Parser.prototype.expression = function(rbp) {
    var left; // ENode
    var t = this.current_token;
    if (t == null)
        throw new ParseException("Expected something at the end",
            this.tokens[this.tokens.length - 1].to + 1);
    this.advance();
    if (t.op == null)
        left = new ENode(t.type, null, t.value, null);
    else if (t.op.nud == null)
        throw new ParseException("Unexpected operator '" + t.op.id + "'", t.from);
    else
        left = t.op.nud(this);
    while (this.current_token != null && this.current_token.op != null &&
            rbp < this.current_token.op.lbp) {
        t = this.current_token;
        this.advance();
        left = t.op.led(this, left);
    }
    return left;
};

/**
 * Advance to the next token,
 * expecting the given operator id if it is provided.
 * Throws a ParseException if a given operator id is not found.
 * @param {string} [id] - Operator id
 */
Parser.prototype.advance = function(id) {
    if (id && (this.current_token == null || this.current_token.op == null ||
            this.current_token.op.id !== id)) {
        if (this.current_token == null)
            throw new ParseException("Expected '" + id + "' at the end",
                this.tokens[this.tokens.length - 1].to + 1);
        else
            throw new ParseException("Expected '" + id + "'", this.current_token.from);
    }
    if (this.token_nr >= this.tokens.length) {
        this.current_token = null;
        return;
    }
    this.current_token = this.tokens[this.token_nr];
    this.token_nr += 1;
};

/**
 * Adds hidden multiplication and unit operators to the token stream
 */
Parser.prototype.addHiddenOperators = function() {
    var multiplication = this.defs.findOperator("*");
    var unit_operator = this.defs.findOperator("`");
    var in_units = false; // we check if we are already in the units to avoid adding two ` operators inside
    var in_exp = false;
    for (var i=0; i<this.tokens.length - 1; i++) {
        var token = this.tokens[i];
        var next_token = this.tokens[i + 1];
        if (this.unit_mode) {
            if (token.value == "`")
                in_units = true;
            else if (in_units) {
                if (token.value == "^")
                    in_exp = true;
                else if (in_exp && token.type == Token.NUMBER)
                    in_exp = false;
                else if (!in_exp && token.type == Token.NUMBER)
                    in_units = false;
                else if (token.type == Token.OPERATOR && "*/^()".indexOf(token.value) == -1)
                    in_units = false;
                else if (token.type == Token.NAME && next_token.value == "(")
                    in_units = false;
            }
        }
        if (
                (token.type == Token.NAME && next_token.type == Token.NAME) ||
                (token.type == Token.NUMBER && next_token.type == Token.NAME) ||
                (token.type == Token.NUMBER && next_token.type == Token.NUMBER) ||
                (token.type == Token.NUMBER && (next_token.value == "(" || next_token.value == "[")) ||
                /*(token.type == Token.NAME && next_token.value == "(") ||*/
                /* name ( could be a function call */
                ((token.value == ")" || token.value == "]") && next_token.type == Token.NAME) ||
                ((token.value == ")" || token.value == "]") && next_token.type == Token.NUMBER) ||
                ((token.value == ")" || token.value == "]") && next_token.value == "(")
           ) {
            // support for things like "(1/2) (m/s)" is complex...
            var units = (this.unit_mode && !in_units && (token.type == Token.NUMBER ||
                (token.value == ")" || token.value == "]")) &&
                (next_token.type == Token.NAME ||
                    ((next_token.value == "(" || next_token.value == "[") && this.tokens.length > i + 2 &&
                    this.tokens[i + 2].type == Token.NAME)));
            if (units) {
                var test_token, index_test;
                if (next_token.type == Token.NAME) {
                    test_token = next_token;
                    index_test = i + 1;
                } else {
                    // for instance for "2 (m/s)"
                    index_test = i + 2;
                    test_token = this.tokens[index_test];
                }
                for (var j=0; j<this.constants.length; j++) {
                    if (test_token.value == this.constants[j]) {
                        units = false;
                        break;
                    }
                }
                if (this.tokens.length > index_test + 1 && this.tokens[index_test + 1].value == "(") {
                    var known_functions = ["pow", "sqrt", "abs", "exp", "factorial", "diff",
                        "integrate", "sum", "product", "limit", "binomial", "matrix",
                        "ln", "log", "log10", "mod", "signum", "ceiling", "floor",
                        "sin", "cos", "tan", "asin", "acos", "atan", "atan2",
                        "sinh", "cosh", "tanh", "asinh", "acosh", "atanh"];
                    for (var j=0; j<known_functions.length; j++) {
                        if (test_token.value == known_functions[j]) {
                            units = false;
                            break;
                        }
                    }
                }
            }
            var new_token;
            if (units) {
                new_token = new Token(Token.OPERATOR, next_token.from,
                    next_token.from, unit_operator.id, unit_operator);
                in_units = true;
            } else {
                new_token = new Token(Token.OPERATOR, next_token.from,
                    next_token.from, multiplication.id, multiplication);
            }
            this.tokens.splice(i+1, 0, new_token);
        }
    }
}

/**
 * Parse the string, returning an ENode tree.
 * @param {string} text - The text to parse.
 * @returns {ENode}
 */
Parser.prototype.parse = function(text) {
    var tokenizer = new Tokenizer(this.defs, text);
    this.tokens = tokenizer.tokenize();
    if (this.tokens.length == 0) {
        return null;
    }
    if (this.implicit_operators) {
        this.addHiddenOperators();
    }
    this.token_nr = 0;
    this.current_token = this.tokens[this.token_nr];
    this.advance();
    var root = this.expression(0);
    if (this.current_token != null) {
        throw new ParseException("Expected the end", this.current_token.from);
    }
    return root;
};

Index: loncom/html/adm/LC_math_editor/src/token.js
+++ loncom/html/adm/LC_math_editor/src/token.js
/*

Copyright (C) 2014  Michigan State University Board of Trustees

The JavaScript code in this page is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version.  The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.

As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.

*/

/**
 * A token from the equation text.
 * @constructor
 * @param {number} type - Token type: Token.UNKNOWN, NAME, NUMBER, OPERATOR
 * @param {number} from - Index of the token's first character
 * @param {number} to - Index of the token's last character
 * @param {string} value - String content of the token
 * @param {Operator} op - The matching operator, possibly null
 */
function Token(type, from, to, value, op) {
    this.type = type;
    this.from = from;
    this.to = to;
    this.value = value;
    this.op = op;
}

Token.UNKNOWN = 0;
Token.NAME = 1;
Token.NUMBER = 2;
Token.OPERATOR = 3;

Index: loncom/html/adm/LC_math_editor/src/tokenizer.js
+++ loncom/html/adm/LC_math_editor/src/tokenizer.js
/*

Copyright (C) 2014  Michigan State University Board of Trustees

The JavaScript code in this page is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version.  The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.

As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.

*/

/**
 * String tokenizer. Recognizes only names, numbers, and parser operators.
 * @constructor
 * @param {Definitions} defs - Operator definitions
 * @param {string} text - The text to tokenize
 */
function Tokenizer(defs, text) {
    this.defs = defs;
    this.text = text;
}

/**
 * Tokenizes the text.
 * Can throw a ParseException.
 * @returns {Array.<Token>}
 */
Tokenizer.prototype.tokenize = function() {
    var c, i, iop, from, tokens, value;
    
    i = 0;
    c = this.text.charAt(i);
    tokens = [];
    
main:
    while (c) {
        from = i;
        
        // ignore whitespace
        if (c <= ' ') {
            i++;
            c = this.text.charAt(i);
            continue;
        }
        
        // check for numbers before operators
        // (numbers starting with . will not be confused with the . operator)
        if ((c >= '0' && c <= '9') ||
                ((c === Definitions.DECIMAL_SIGN_1 || c === Definitions.DECIMAL_SIGN_2) &&
                (this.text.charAt(i+1) >= '0' && this.text.charAt(i+1) <= '9'))) {
            value = '';
            
            if (c !== Definitions.DECIMAL_SIGN_1 && c !== Definitions.DECIMAL_SIGN_2) {
                i++;
                value += c;
                // Look for more digits.
                for (;;) {
                    c = this.text.charAt(i);
                    if (c < '0' || c > '9') {
                        break;
                    }
                    i++;
                    value += c;
                }
            }
            
            // Look for a decimal fraction part.
            if (c === Definitions.DECIMAL_SIGN_1 || c === Definitions.DECIMAL_SIGN_2) {
                i++;
                value += c;
                for (;;) {
                    c = this.text.charAt(i);
                    if (c < '0' || c > '9') {
                        break;
                    }
                    i += 1;
                    value += c;
                }
            }
            
            // Look for an exponent part.
            if (c === 'e' || c === 'E') {
                i++;
                value += c;
                c = this.text.charAt(i);
                if (c === '-' || c === '+') {
                    i++;
                    value += c;
                    c = this.text.charAt(i);
                }
                if (c < '0' || c > '9') {
                    // syntax error in number exponent
                    throw new ParseException("syntax error in number exponent", from, i);
                }
                do {
                    i++;
                    value += c;
                    c = this.text.charAt(i);
                } while (c >= '0' && c <= '9');
            }
            
            /* this is not necessary, as the parser will not recognize the tokens
               if it is not accepted, and if bad syntax is accepted a * operator will be added
            // Make sure the next character is not a letter.
            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
                // syntax error in number
                throw new ParseException("syntax error in number", from, i);
            }
            */
            
            // Convert the string value to a number. If it is finite, then it is a good token.
            var n = +value.replace(Definitions.DECIMAL_SIGN_1, '.').replace(Definitions.DECIMAL_SIGN_2, '.');
            if (isFinite(n)) {
                tokens.push(new Token(Token.NUMBER, from, i - 1, value, null));
                continue;
            } else {
                // syntax error in number
                throw new ParseException("syntax error in number", from, i);
            }
        }
        
        // check for operators before names (they could be confused with
        // variables if they don't use special characters)
        for (iop = 0; iop < this.defs.operators.length; iop++) {
            var op = this.defs.operators[iop];
            if (this.text.substring(i, i+op.id.length) === op.id) {
                i += op.id.length;
                c = this.text.charAt(i);
                tokens.push(new Token(Token.OPERATOR, from, i - 1, op.id, op));
                continue main;
            }
        }
        
        // names
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
            value = c;
            i++;
            for (;;) {
                c = this.text.charAt(i);
                if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
                        (c >= '0' && c <= '9') || c === '_') {
                    value += c;
                    i++;
                } else {
                    break;
                }
            }
            tokens.push(new Token(Token.NAME, from, i - 1, value, null));
            continue;
        }
        
        // unrecognized operator
        throw new ParseException("unrecognized operator", from, i);
    }
    return tokens;
};

Index: loncom/html/adm/LC_math_editor/src/ui.js
+++ loncom/html/adm/LC_math_editor/src/ui.js
/*

Copyright (C) 2014  Michigan State University Board of Trustees

The JavaScript code in this page is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version.  The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.

As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.

*/

"use strict";

var handleChange = function(math_object) {
    // math_object has 3 fields: ta, output_node, oldtxt
    // we need to pass this object instead of the values because oldtxt will change
    var ta, output_node, txt, parser, output, root, test1, test2;
    ta = math_object.ta;
    output_node = math_object.output_node;
    txt = ta.value;
    
    // automatically add brackets to something like "1;2;3", for LON-CAPA:
    // NOTE: this is ugly and sometimes adds brackets to error messages
    test1 = '';
    test2 = txt;
    while (test2 != test1) {
      test1 = test2;
      test2 = test1.replace(/\[[^\[\]]*\]/g, '');
    }
    if (test2.split("[").length == test2.split("]").length) {
      test1 = '';
      while (test2 != test1) {
        test1 = test2;
        test2 = test1.replace(/\([^\(\)]*\)/g, '');
      }
      if (test2.split("(").length == test2.split(")").length) {
        if (test2.indexOf(Definitions.ARG_SEPARATOR) != -1) {
          txt = '['+txt+']';
        }
      }
    }
    
    if (txt != math_object.oldtxt) {
        math_object.oldtxt = txt;
        while (output_node.firstChild != null)
            output_node.removeChild(output_node.firstChild);
        output_node.removeAttribute("title");
        if (txt != "") {
            parser = math_object.parser;
            try {
                root = parser.parse(txt);
                if (root != null) {
                    var math = document.createElement("math");
                    math.setAttribute("display", "block");
                    math.appendChild(root.toMathML());
                    output_node.appendChild(math);
                    MathJax.Hub.Queue(["Typeset", MathJax.Hub, output_node]);
                }
            } catch (e) {
                output = "error: " + e;
                output_node.setAttribute("title", output);
                if (e instanceof ParseException) {
                    output_node.appendChild(document.createTextNode(txt.substring(0, e.from)));
                    var span = document.createElement('span');
                    span.appendChild(document.createTextNode(txt.substring(e.from, e.to + 1)));
                    span.className = 'math-error';
                    output_node.appendChild(span);
                    if (e.to < txt.length - 1) {
                        output_node.appendChild(document.createTextNode(txt.substring(e.to + 1)));
                    }
                } else {
                    var tn = document.createTextNode(output);
                    output_node.appendChild(tn);
                }
            }
        }
    }
}

var init_done = false;

/*
  Looks for elements with the "math" class, and
  adds a preview div afterward which is updated automatically.
*/
var initEditors = function() {
    if (init_done)
        return;
    init_done = true;
    var math_objects = [];
    var math_inputs = document.getElementsByClassName('math');
    for (var i=0; i<math_inputs.length; i++) {
        var ta = math_inputs[i];
        if (ta.nodeName == "TEXTAREA" || ta.nodeName == "INPUT") {
            var output_node = document.createElement("span");
            output_node.setAttribute("style", "display:none");
            if (ta.nextSibling)
                ta.parentNode.insertBefore(output_node, ta.nextSibling);
            else
                ta.parentNode.appendChild(output_node);
            var hideNode = function(node) {
                return function(e) { node.setAttribute("style", "display:none"); };
            };
            var showNode = function(node) {
                return function(e) { node.setAttribute("style", "display: inline-block; background-color: #FFFFE0"); };
            };
            ta.addEventListener("blur", hideNode(output_node), false);
            ta.addEventListener("focus", showNode(output_node), false);
            var implicit_operators = (ta.getAttribute("data-implicit_operators") === "true");
            var unit_mode = (ta.getAttribute("data-unit_mode") === "true");
            var constants = ta.getAttribute("data-constants");
            if (constants)
                constants = constants.split(/[\s,]+/);
            var oldtxt = "";
            math_objects[i] = {
                "ta": ta,
                "output_node": output_node,
                "oldtxt": oldtxt,
                "parser": new Parser(implicit_operators, unit_mode, constants)
            };
            var changeObjectN = function(n) {
                return function(e) {
                  var obj = math_objects[n];
                  if (document.activeElement == obj.ta) {
                    // this is useful if there is data in the field with the page default focus
                    // (there might not be a focus event for the active element)
                    obj.output_node.setAttribute("style", "display: inline-block; background-color: #FFFFE0");
                  }
                  handleChange(obj);
                };
            };
            var startChange = changeObjectN(i);
            if (ta.value != oldtxt)
                startChange(); // process non-empty fields even though they are not visible yet
            ta.addEventListener('change', startChange, false);
            ta.addEventListener('keyup', startChange, false);
        }
    }
}



More information about the LON-CAPA-cvs mailing list