Source: mathex/res-otto/js/mathex.js

/**
 * @summary Mathex Library namespace
 * @description This is a library which provides a set of classes which work together with mathjax (http://www.mathjax.org) in order to reproduce interactive math exercises
 * @license MIT-style license
 * @copyright 2014 Otto srl
 * @author abidibo <dev@abidibo.net> (http://www.abidibo.net)
 * @requires mathjax
 * @requires mootools-core>=1.4
 * @requires mootools-more>=1.4
 * @namespace
 */
var mathex;
if (!mathex) mathex = {};
else if( typeof mathex != 'object') {
  throw new Error('mathex already exists and is not an object');
}

/**
 * @summary Library configuration object
 * @memberof mathex
 * @property {Boolean} font_ctrl Whether or not to activate the font size widget
 */
mathex.config = {
  font_ctrl: true
};

/**
 * @namespace
 * @description Common operations object, this is not a class, rather an object which stores common used methods.
 * @memberof mathex
 */
mathex.Shared = {
  /**
   * @summary Parses a mathex template
   * @description Parses mathjax tags converting them to mathjax syntax, replaces input fields inside mathjax tags and activates toggling images
   * @memberof mathex.Shared
   * @method
   * @param {String} tpl The mathex template
   * @param {Object} [inputs] Object describing the field inputs
   * @return {String} The parsed template
   */
  parseTpl: function(tpl, inputs) {
    // get mathjax blocks
    var math_rexp = new RegExp("{%(.*?)%}", "gim");
    // substitute with script delimiters
    var final = tpl.replace(math_rexp, "<script type=\"math/tex\">$1</script>");
    // get inputs
    var inputs_rexp = new RegExp("\\FormInput([0-9]*)", "gm");
    var m_index = 0;
    while(inputs_rexp.test(final)) {
      inputs_rexp.lastIndex = m_index; // return to previous match since the test function change lastIndex property
      var matches = inputs_rexp.exec(final);
      m_index = inputs_rexp.lastIndex // save lastIndex
      var input_index = matches[1];
      var input_replace_rexp = new RegExp("\\FormInput" + input_index, "");
      try{
        var rstring = "\FormInput[" + inputs[input_index.toInt()].size + "][input][]{field_" + input_index + "}";
      }
      catch(err) {
        console.log(err);
        console.log('undefined input object');
        var rstring = '';
      }
      final = final.replace(input_replace_rexp, rstring);
    }

    // toggle images
    var img_rexp = new RegExp("<img class=\"toggle\" src=\"(.*?)\"(.*?)>", 'g');
    final = final.replace(img_rexp, "<img class=\"toggle\" src=\"$1\" onclick=\"mathex.Shared.toggleImage(this)\"$2>");

    return final;
  },
  /**
   * @summary Toggles an image (toggles the '_toggle' suffix of the src attribute)
   * @memberof mathex.Shared
   * @method
   * @param {Object} img The mootools img element
   * @return void
   */
  toggleImage: function(img) {
    var src = img.get('src');
    var rexp = new RegExp("([a-zA-Z0-9-_./]*)\\.([a-zA-Z]*)");
    var matches = rexp.exec(src);
    var path = matches[1];
    var extension = matches[2];
    var toggle_rexp = new RegExp("(.*?)_toggle");
    if(toggle_rexp.test(path)) {
      var nsrc = path.replace(toggle_rexp, "$1") + '.' + extension;
    }
    else {
      var nsrc = path + '_toggle.' + extension;
    }
    img.src = nsrc;
  },
  /**
   * @summary Shows a message in a layer above the document
   * @memberof mathex.Shared
   * @method
   * @param {String} msg The text message
   * @param {String} css A style class to apply to the message container
   * @param {Function} [callback] A function to call when the layer is closed (click over the layer or the overlay)
   * @return void
   */
  showMessage: function(msg, css, callback) {

    var doc_dim = document.getScrollSize();

    this.overlay = new Element('div');
    this.overlay.setStyles({
      position: 'absolute',
      top: 0,
      left: 0,
      background: '#000',
      'z-index': 1,
      width: doc_dim.x,
      height: doc_dim.y,
      opacity: 0.6
    });
    this.overlay.inject(document.body);

    var vp = this.getViewport();

    this.message_container = new Element('div#message.' + css);
    this.message_container.setStyles({
      position: 'absolute',
      top: (vp.cY - 20) + 'px',
      left: (vp.cX - 110) + 'px',
      background: '#fff',
      'z-index': 2,
      width: 220 + 'px',
      padding: '10px'
    });
    this.message_container.inject(document.body);
    this.message_container.set('html', '<p>' + msg + '</p>');

    this.overlay.addEvent('click', function(evt) {
      this.message_container.dispose();
      this.overlay.dispose();
      if(callback) callback();
    }.bind(this));

    this.message_container.addEvent('click', function(evt) {
      this.message_container.dispose();
      this.overlay.dispose();
      if(callback) callback();
    }.bind(this));
  },
  /**
   * @summary Gets the viewport coordinates of the current window (width, height, left offest, top offset, coordinates of the center point).
   * @memberof mathex.Shared
   * @method
   * @return {Object} Viewport coordinates
   * @example
   *      // returned object
   *      {'width':width, 'height':height, 'left':left, 'top':top, 'cX':cX, 'cY':cY}
   */
  getViewport: function() {

    var document_coords = document.getCoordinates();
    var document_scroll = document.getScroll();
    var width = document_coords.width;
    var height = document_coords.height;
    var left = document_scroll.x;
    var top = document_scroll.y;
    var cX = document_coords.width / 2 + document_scroll.x;
    var cY = document_coords.height / 2 + document_scroll.y;

    return {'width': width, 'height': height, 'left': left, 'top': top, 'cX': cX, 'cY': cY};

  },
  /**
   * @summary Add a widget in the widget container
   * @memberof mathex.Shared
   * @method
   * @param {widget} widget The widget mootools element
   * @param {String} position Position respect to the container (top, bottom)
   * @return void
   */
  addWidget: function(widget, position) {
    widget.inject($('widgets'), position);
  },
  /**
   * @summary Removes a widget
   * @memberof mathex.Shared
   * @method
   * @param {widget} widget The widget mootools element
   * @return void
   */
  removeWidget: function(widget) {
    widget.dispose();
  },
  /**
   * @summary Creates a player widget in the top of the #container div, given an audio object. Removes it if the audio object is null
   * @memberof mathex.Shared
   * @method
   * @param {Object} audio_obj The audio object
   * @param {String} [audio_obj.mp3] Path to the mp3 file
   * @param {String} [audio_obj.ogg] Path to the ogg file
   * @return void
   */
  playerWidget: function(audio_obj) {
    if(audio_obj === null) {
      if(typeof $('container').getElements('audio')[0] != 'undefined') {
        $('container').getElements('audio')[0].dispose();
      }
      return true;
    }
    var audio = new Element('audio[controls]');
    if(typeof audio_obj.mp3 != 'undefined') {
      var mp3_source = new Element('source[src=' + audio_obj.mp3 + '][type=audio/mp3]').inject(audio);
    }
    if(typeof audio_obj.ogg != 'undefined') {
      var ogg_source = new Element('source[src=' + audio_obj.ogg + '][type=audio/ogg]').inject(audio);
    }
    audio.inject($('container'), 'top');
  },
  /**
   * @summary Creates a font-size controller widget and places it inside the widget container
   * @memberof mathex.Shared
   * @method
   * @return void
   */
  fontWidget: function() {
    var regular = parseFloat($(document.body).getStyle('font-size'));
    var fm = new Element('span#font_minus.font_controller')
      .set('html', 'A-')
      .addEvent('click', function() {
        size = $(document.body).getStyle('font-size').toInt();
        size = size > 12 ? size - 1 : size;
        $(document.body).setStyle('font-size', size);
      })
      .inject($('widgets'));
    var f = new Element('span#font_regular.font_controller')
      .set('html', 'A')
      .addEvent('click', function() {
        $(document.body).setStyle('font-size', regular);
      })
      .inject($('widgets'));
    var fp = new Element('span#font_plus.font_controller')
      .set('html', 'A+')
      .addEvent('click', function() {
        size = $(document.body).getStyle('font-size').toInt();
        size = size < 26 ? size + 1 : size;
        $(document.body).setStyle('font-size', size);
      })
      .inject($('widgets'));
  },
  /**
   * @summary Creates a calculator widget and places it inside the widget container
   * @memberof mathex.Shared
   * @method
   * @return void
   */
  calculatorWidget: function() {
    var c;
    var Calculator = new Class({
      initialize: function() {
        this.render();
        this.addEvents();
        this.first_term = null;
        this.new_term = true;
        this.operation = null;
      },
      render: function() {
        var table = new Element('table')
          .adopt(
            new Element('tr').adopt(
              new Element('td.operation').set('text', '√'),
              new Element('td.operation').set('html', 'x<sup>y</sup>'),
              new Element('td.operation').set('text', '%'),
              new Element('td.clear').set('text', 'CE')
            ),
            new Element('tr').adopt(
              new Element('td.number').set('text', '7'),
              new Element('td.number').set('text', '8'),
              new Element('td.number').set('text', '9'),
              new Element('td.operation').set('text', '÷')
            ),
            new Element('tr').adopt(
              new Element('td.number').set('text', '4'),
              new Element('td.number').set('text', '5'),
              new Element('td.number').set('text', '6'),
              new Element('td.operation').set('text', '×')
            ),
            new Element('tr').adopt(
              new Element('td.number').set('text', '1'),
              new Element('td.number').set('text', '2'),
              new Element('td.number').set('text', '3'),
              new Element('td.operation').set('text', '-')
            ),
            new Element('tr').adopt(
              new Element('td.number').set('text', '0'),
              new Element('td.dot').set('text', '.'),
              new Element('td.equal').set('text', '='),
              new Element('td.operation').set('text', '+')
            )
          );
        var vp = mathex.Shared.getViewport();
        this.container = new Element('div#calculator')
          .inject(document.body)
          .setStyles({
            position: 'fixed',
            top: vp.cY - 150,
            left: vp.cX - 106
          })
          .adopt(this.display = new Element('div#display').set('text', 0), table);

          var doc_dimensions = document.getCoordinates();

          var drag_instance = new Drag(this.container, {
            'limit':{'x':[0, (doc_dimensions.width-this.container.getCoordinates().width)], 'y':[0, ]}
          });
      },
      addEvents: function() {
        this.container.addEvent('click', this.click.bind(this));
      },
      click: function(evt) {
        var target = evt.target;
        // calculate
        if(target.get('class') == 'equal') {
          var result = this.calculate();
          if(result !== null) {
            this.display.set('text', result);
          }
        }
        // clear
        else if(target.get('class') == 'clear') {
          this.clear();
        }
        // operation
        else if(target.get('class') == 'operation') {

          var text = target.get('text');

          // minus can be used for negative numbers
          if(text == '-' && (this.new_term && (this.operation || this.first_term == null))) {
            this.display.set('text', '-');
            this.new_term = false;
            return null;
          }

          // sqrt and % are calculated over only the first term
          if(text == '√' || text == '%') {
            if(this.operation) {
              this.display.set('text', this.first_term = this.calculate());
            }
            else {
              this.first_term = parseFloat(this.display.get('text'));
            }
            this.operation = text;
            this.display.set('text', this.calculate());
            return null;
          }

          if(this.first_term == null) {
            this.first_term = parseFloat(this.display.get('text'));
          }
          else {
            this.display.set('text', this.first_term = this.calculate());
          }

          this.operation = text;
          this.new_term = true;

        }
        else if(target.get('class') == 'number' || target.get('class') == 'dot')  {
          if(this.new_term) {
            this.display.set('text', target.get('text'));
            this.new_term = false;
          }
          else {
            // only 0 or 1 dot character
            if(this.display.get('text').length < 14 && (target.get('class') != 'dot' || !/\./.test(this.display.get('text')))) {
              this.display.appendText(target.get('text'));
            }
          }
        }
      },
      clear: function() {
        this.first_term = null;
        this.operation = null;
        this.display.set('text', 0);
        this.new_term = true;
      },
      calculate: function() {
        if(this.first_term == null || this.operation == null) return null;
        this.second_term = parseFloat(this.display.get('text'));

        var result;

        if(this.operation == '√') {
          result = Math.sqrt(this.first_term);
        }
        else if(this.operation == '%') {
          result = this.first_term / 100;
        }
        else if(this.operation == '+') {
          result = this.first_term + this.second_term;
        }
        else if(this.operation == '-') {
          result = this.first_term - this.second_term;
        }
        else if(this.operation == '×') {
          result = this.first_term * this.second_term;
        }
        else if(this.operation == '÷') {
          result = this.first_term / this.second_term;
        }
        else if(this.operation == 'xy') {
          result = Math.pow(this.first_term, this.second_term);
        }

        this.first_term = null;
        this.operation = null;
        return this.format(result);
      },
      format: function(result) {
        var s = result.toString();
        var parts = s.split('.');
        var integer = parts[0];
        var decimal = parts.length == 2 ? parts[1] : null;
        if(integer && integer.length > 14) {
            return result.toExponential(8);
        }
        else if(integer && decimal) {
          if(integer.length + decimal.length < 14) {
            return s;
          }
          else if(integer.length < 14) {
            if(/e/.test(decimal)) {
              return result.toExponential(9 - integer.length);
            }
            else {
              var factor = Math.pow(10, (13 - integer.length));
              return Math.round(result * factor) / factor;
            }
          }
          else if(integer.length = 14) {
            return Math.round(result);
          }
        }
        else {
          return s;
        }
      },
      remove: function() {
        this.container.destroy();
      },
      hide: function() {
        this.container.style.display = 'none';
      },
      show: function() {
        this.container.style.display = 'block';
      }
    });
    var calc = new Element('span#calc')
      .set('html', 'calc')
      .addEvent('click', function() {
        if(typeof c != 'undefined') {
          if(c.container.style.display == 'none') c.show();
          else c.hide();
        }
        else {
          c = new Calculator();
        }
      })
      .inject($('widgets'));
  },
  /**
   * @summary Checks if the given value is correct agains the result considering the type cast
   * @memberof mathex.Shared
   * @method
   * @param {String} type The type cast ('float', 'int', 'string_case')
   * @param {Mixed} result The right result
   * @param {Mixed} value The value to check
   * @return {Boolean} The check result
   */
  checkResult: function(type, result, value) {
    if(type == 'float') {
      return parseFloat(result.replace(',', '.')) === parseFloat(value.replace(',', '.'));
    }
    else if(type == 'int') {
      return parseInt(result) === parseInt(value);
    } 
    else if(type == 'string_case') {
      return result === value;
    }
    else {
      return result.toLowerCase() === value.toLowerCase();
    }
  }
}

/***********************************************************************
 *
 *  EXERCISES
 *
 ***********************************************************************/

/**
 * @summary Exercises Router Class
 * @classdesc Given some steps the Router manages the navigation through the steps till the end of the exercise.
 * @memberof mathex
 * @constructs mathex.Router
 * @param {Object} [options] Router options
 * @param {Boolean} [options.widgets=true] Whether or not to create the font and calculator widgets
 * @return {Object} A Router instance
 */
mathex.Router = function(options) {
  this.steps = [];
  this.current = null;
  this.options = typeof options != 'undefined' ? options : { widgets: true };

  /**
   * @summary Initializes a Router instance
   * @memberof mathex.Router.prototype
   * @method init
   * @param {Array} s The exercise's steps
   * @return void
   */
  this.init = function(s) {
    if(this.options.widgets) {
      // widgets
      if(mathex.config.font_ctrl) {
        mathex.Shared.fontWidget();
      }
      mathex.Shared.calculatorWidget();
    }
    this.steps = s;
  };
  /**
   * @summary Adds steps
   * @memberof mathex.Router.prototype
   * @method addSteps
   * @param {Array} s steps
   * @return void
   */
  this.addSteps = function(s) {
    this.steps = this.steps.append(s);
  };
  /**
   * @summary Gets the steps
   * @memberof mathex.Router.prototype
   * @method getSteps
   * @return {Array} The steps
   */
  this.getSteps = function() {
    return this.steps;
  };
  /**
   * @summary Gets the current step index
   * @memberof mathex.Router.prototype
   * @method getCurrent
   * @return {Number} The current step index
   */
  this.getCurrent = function() {
    return this.current;
  };
  /**
   * @summary Starts the execution of a step
   * @memberof mathex.Router.prototype
   * @method startStep
   * @param {Number} [index=0] The index of the step to be executed, default 0
   */
  this.startStep = function(index) {
    index = index ? index: 0;
    try {
      var step = this.steps[index];
      this.current = index;
      step.run(this);
    }
    catch(err) {
      console.log(err);
      console.log('step undefined or not a step');
    }
  };
  /**
   * @summary Ends the execution of a step
   * @memberof mathex.Router.prototype
   * @method endStep
   * @param {Number} [callback] A callback function to call if it was the last step
   * @return void
   */
  this.endStep = function(callback) {
    if(this.current === this.steps.length - 1 ) {
      if(typeof callback != 'undefined') {
        callback();
      }
    }
    else {
      this.startStep(this.current + 1);
    }
  };
  /**
   * @summary Executes all the steps in succession. Debugging purposes.
   * @memberof mathex.Router.prototype
   * @method allSteps
   * @return void
   */
  this.allSteps = function() {
    this.current = 0;
    while(this.current <= this.steps.length - 1) {
      this.startStep(this.current);
      this.current++;
    }
  }
}

/**
 * @namespace
 * @summary Exercises step superclass,  an object which all steps have as their prototype.
 * @memberof mathex
 */
mathex.Step = {

  /**
   * @summary Checks the result of a text field
   * @memberof mathex.Step.prototype
   * @method checkFieldResult
   * @param {Object} field The mootools input field object
   * @param {Mixed} result The right result
   * @param {Object} fieldobj The input object
   */
  checkFieldResult: function(field, result, fieldobj) {

    this.removeInputEvents();

    if(this.end_message) {
      var callback = function() {
        mathex.Shared.showMessage(this.end_message, 'message', null);
      }.bind(this)
    }
    else {
      var callback = null;
    }

    if(!field.retrieve('errors')) {
      field.store('errors', 0);
    }

    if(!mathex.Shared.checkResult(fieldobj.type, result, field.value)){
      if(field.retrieve('errors') != 1) {
        if(typeof fieldobj != 'undefined' && typeof fieldobj.comment != 'undefined') {
          mathex.Shared.showMessage(fieldobj.comment, 'message', this.addInputEvents.bind(this));
        }
        else {
          mathex.Shared.showMessage('Risposta errata, riprova', 'error', this.addInputEvents.bind(this));
        }
        field.blur();
        field.value = '';
        field.store('errors', 1);
      }
      else {
        mathex.Shared.showMessage('Risposta errata. La risposta esatta è ' + result, 'failed', callback);
        field.value = result;
        this.deactivate();
        this.router.endStep();
      }
    }
    else {
      mathex.Shared.showMessage('Risposta esatta', 'success', callback);
      this.deactivate();
      this.router.endStep();
    }
  }
}

/**
 * @summary Exercises - Text plus one active field
 * @memberof mathex
 * @constructs mathex.TextFieldStep
 * @extends mathex.Step
 * @param {String} tpl The exercise template.<br />
 *                        <p>The math to be parsed by mathjax (latex syntax) must be placed inside the tag {%%}, i.e. {% 2^4=16 %}</p>
 *                        <p>The input fields inside the math must be formatted this way: \\FormInput2, where 2 is the id of the input which is described through the inputs parameter.</p>
 * @param {Object} inputs The object describing the inputs in the template
 * @param {String} [end_message] A message to be displayed at the end of the step
 * @param {Object} [options] The step options
 * @param {Boolean} [options.container=true] Whether or not to insert the exercise text inside a div container
 * @return {Object} TextFieldStep instance
 *
 * @example
 *  var step1 = new mathex.TextFieldStep(
 *    '<h3> Show that {% 2^7 : 2^4 = 2^(7-4) = 2^3 %}</h3>' + 
 *    '<p>Demonstration:</p>' + 
 *    '<p>{% 2^7 = \\FormInput0 %}</p>' + 
 *    '<p>{% 2^4 = \\FormInput1 %}</p>' +
 *    '<p>etc...</p>',
 *    {
 *      0: {
 *        size: 3,
 *        active: true,
 *        result: '128',
 *        type: 'string'
 *      },
 *      1: {
 *        size: 2,
 *        active: false,
 *      }
 *    },
 *    'a message'
 *  );
 *
 */
mathex.TextFieldStep = function(tpl, inputs, end_message, options) {

  this.container = options && typeof options.container != 'undefined' ? options.container : true;

  /**
   * @summary Executes the step
   * @description Renders the template and activates the input fields
   * @memberof mathex.TextFieldStep.prototype
   * @method run
   * @param {Object} router a mathex.Router instance
   * @return void
   */
  this.run = function(router) {
    this.tpl = mathex.Shared.parseTpl(tpl, inputs);
    this.inputs = inputs;
    this.end_message = typeof end_message == 'undefined' ? null : end_message;
    var self = this;
    this.router = router;
    if(this.container) {
      var div = new Element('div').set('html', this.tpl).inject($('container'), 'bottom');
    }
    else {
      $('container').set('html', $('container').get('html') + this.tpl);
    }
    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);
    MathJax.Hub.Queue(function() {
      self.addInputEvents();
    });
  };
  /**
   * @summary Adds events to the input fields
   * @memberof mathex.TextFieldStep.prototype
   * @method addInputEvents
   * @return void
   */
  this.addInputEvents = function() {
    var self = this;
    Object.each(this.inputs, function(input, index) {
      var input_obj = document.id('field_' + index);
      if(!input.active) {
        input_obj.setProperty('readonly', 'readonly').addClass('disabled');
      }
      else {
        input_obj.addEvent('keydown', self.keyhandler = function(evt) {
          if(evt.key == 'enter') {
            self.checkFieldResult(input_obj, input.result, input);
          }
        });
      }
    }.bind(this));
  }
  /**
   * @summary Removes events from active input fields
   * @memberof mathex.TextFieldStep.prototype
   * @method removeInputEvents
   * @return void
   */
  this.removeInputEvents = function() {
    var self = this;
    Object.each(this.inputs, function(input, index) {
      var input_obj = document.id('field_' + index);
      if(input.active) {
        input_obj.removeEvent('keydown', self.keyhandler);
      }
    }.bind(this));
  }
  /**
   * @summary Deactivates all inputs
   * @memberof mathex.TextFieldStep.prototype
   * @method deactivate
   * @return void
   */
  this.deactivate = function() {
    Object.each(this.inputs, function(input, index) {
      var input_obj = document.id('field_' + index);
      if(input.active) {
        input_obj.removeEvents('keydown');
        input_obj.setProperty('readonly', 'readonly');
      }
    }.bind(this))
  }
}
mathex.TextFieldStep.prototype = mathex.Step;

/**
 * @summary Exercises - Text plus one radio input
 * @memberof mathex
 * @constructs mathex.TextChoiceFieldStep
 * @extends mathex.Step
 * @param {String} tpl The exercise template.<br />
 *                        <p>The math to be parsed by mathjax must be placed inside the tag {%%}, i.e. {% 2^4=16 %}</p>
 *                        <p>The available choices must be written this way: [[0]] my choice. Then the [[0]] is parsed and a radio buton is created.</p>
 * @param {Number} result The index of the correct radio answer
 * @param {String} [end_message] A message to be displayed at the end of the step
 * @param {Object} [options] The step options
 * @param {Boolean} [options.container=true] Whether or not to insert the exercise text inside a div container
 * @return {Object} TextChoiceFieldStep instance
 * @example
 *  var step = new mathex.TextChoiceFieldStep(
 *    '<h3>Title</h3>' + 
 *    '<p>Which color is yellow?</p>' + 
 *    '<ul>' + 
 *    '<li>[[0]] red</li>' + 
 *    '<li>[[1]] {% 2^7 = 32 %}</li>' + 
 *    '<li>[[2]] yellow</li>' + 
 *    '</ul>',
 *    2
 *  );
 */
mathex.TextChoiceFieldStep = function(tpl, result, end_message, options) {

  this.container = options && typeof options.container != 'undefined' ? options.container : true;
  /**
   * @summary Executes the step
   * @description Renders the template and activates radio buttons
   * @memberof mathex.TextChoiceFieldStep.prototype
   * @method run
   * @param {Object} router a mathex.Router instance
   * @return void
   */
  this.run = function(router) {
    this.errors = 0;
    this.string = String.uniqueID();
    this.tpl = mathex.Shared.parseTpl(tpl, []);
    var radio_rexp = new RegExp("\\[\\[([0-9]*?)\\]\\]", "gim");
    this.tpl = this.tpl.replace(radio_rexp, "<input type=\"radio\" name=\"radio_" + this.string + "\" id=\"radio_$1\" />");
    this.result = result;
    this.end_message = typeof end_message == 'undefined' ? null : end_message;
    var self = this;
    this.router = router;
    if(this.container) {
      var div = new Element('div').set('html', this.tpl).inject($('container'), 'bottom');
    }
    else {
      $('container').set('html', $('container').get('html') + this.tpl);
    }
    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);
    MathJax.Hub.Queue(function() {
      self.addInputEvents();
    });
  };
  /**
   * @summary Checks the user answer
   * @memberof mathex.TextChoiceFieldStep.prototype
   * @method checkChoiceFieldResult
   * @param {Object} field The mootools radio button object
   * @param {Number} id The index of the clicked radio button
   * @return void
   */
  this.checkChoiceFieldResult = function(field, id) {
    this.removeInputEvents();

    if(this.end_message) {
      var callback = function() {
        mathex.Shared.showMessage(this.end_message, 'message', null);
      }.bind(this)
    }
    else {
      var callback = null;
    }

    if(!(id.toInt() === this.result || this.errors == 1)) {
      var x = new Element('span.x').set('html', '×').inject(document.id('radio_' + id).setStyle('display', 'none'), 'before');
      mathex.Shared.showMessage('Risposta errata, riprova', 'error', this.addInputEvents.bind(this));
      field.removeProperty('checked');
      this.errors = 1;
    }
    else {

      if(id.toInt() === this.result) {
        //var v = new Element('span.v').set('html', '✔').replaces(document.id('radio_' + id));
        var v = new Element('span.v').set('html', '✔').inject(document.id('radio_' + this.result).setStyle('display', 'none'), 'before');
        mathex.Shared.showMessage('Risposta esatta', 'success', callback);
      }
      else {
        var x = new Element('span.x').set('html', '×').inject(document.id('radio_' + id).setStyle('display', 'none'), 'before');
        var v = new Element('span.v').set('html', '✔').inject(document.id('radio_' + this.result).setStyle('display', 'none'), 'before');
        //var v = new Element('span.v').set('html', '✔').replaces(document.id('radio_' + this.result));
        mathex.Shared.showMessage('Risposta errata.', 'failed', callback);
      }
      this.deactivate();
      this.router.endStep();
    }

  }
  /**
   * @summary Adds events to the radio buttons
   * @memberof mathex.TextChoiceFieldStep.prototype
   * @method addInputEvents
   * @return void
   */
  this.addInputEvents = function() {
    var self = this;
    document.getElements('input[name=radio_' + this.string + ']').each(function(input, index) {
      var input_obj = input;
      input_obj.addEvent('click', self.clickhandler = function(evt) {
        self.checkChoiceFieldResult(input_obj, input_obj.get('id').replace('radio_', ''));
      });
    }.bind(this));
  }
  /**
   * @summary Removes events from the radio buttons
   * @memberof mathex.TextChoiceFieldStep.prototype
   * @method removeInputEvents
   * @return void
   */
  this.removeInputEvents = function() {
    var self = this;
    Object.each(this.inputs, function(input, index) {
      var input_obj = document.id('radio_' + index);
      input_obj.removeEvent('click', self.clickhandler);
    }.bind(this));
  }
  /**
   * @summary Deactivates all inputs
   * @memberof mathex.TextChoiceFieldStep.prototype
   * @method deactivate
   * @return void
   */
  this.deactivate = function() {
    document.getElements('input[name=radio_' + this.string + ']').each(function(input, index) {
      var input_obj = input;
      input_obj.removeEvents('click');
      input_obj.setProperty('readonly', 'readonly');
    }.bind(this))
  }
}
mathex.TextChoiceFieldStep.prototype = mathex.Step;

/**
 * @summary Exercises - Text plus one select input
 * @memberof mathex
 * @constructs mathex.TextSelectFieldStep
 * @extends mathex.Step
 * @params {String} tpl The exercise template.<br />
 *                        <p>The math to be parsed by mathjax must be placed inside the tag {%%}, i.e. {% 2^4=16 %}</p>
 *                        <p>The select input field must be inserted this way: [[]]. Its options are described through the select_options parameter.
 * @params {Array} select_options The select options
 * @params {String} result The correct option
 * @params {String} [end_message] A message to be displayed at the end of the step
 * @params {Object} [options] The step options
 * @params {Boolean} [options.container=true] Whether or not to insert the exercise text inside a div container
 * @return {Object} TextSelectFieldStep instance
 * @example
 *
 *  var step4 = new mathex.TextSelectFieldStep(
 *    '<h3>Title</h3>' + 
 *    '<p>Which color is yellow?</p>' + 
 *    '<p>[[]] choose one</p>',
 *    ['red', 'orange', 'yellow'],
 *    'yellow'
 *  );
 */
mathex.TextSelectFieldStep = function(tpl, select_options, result, end_message, options) {

  this.container = options && typeof options.container != 'undefined' ? options.container : true;
  /**
   * @summary Populates the select field with the options
   * @memberof mathex.TextSelectFieldStep.prototype
   * @method populateSelect
   * @return void
   */
  this.populateSelect = function() {
    select_options.each(function(option) {
      var opt = new Element('option[value=' + option +']').set('text', option).inject(document.id('select_' + this.string), 'bottom');
    }.bind(this));
  };
  /**
   * @summary Executes the step
   * @description Renders the template and activates the select input
   * @memberof mathex.TextSelectFieldStep.prototype
   * @method run
   * @param {Object} router a mathex.Router instance
   * @return void
   */
  this.run = function(router) {
    this.errors = 0;
    this.string = String.uniqueID();
    this.tpl = mathex.Shared.parseTpl(tpl, []);
    var select_rexp = new RegExp("\\[\\[\\]\\]", "gim");
    this.tpl = this.tpl.replace(select_rexp, "<select id=\"select_" + this.string + "\"><option value=''></option></select>");
    this.result = result;
    this.end_message = typeof end_message == 'undefined' ? null : end_message;
    var self = this;
    this.router = router;
    if(this.container) {
      var div = new Element('div').set('html', this.tpl).inject($('container'), 'bottom');
    }
    else {
      $('container').set('html', $('container').get('html') + this.tpl);
    }
    this.populateSelect();
    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);
    MathJax.Hub.Queue(function() {
      self.addInputEvents();
    });
  };
  /**
   * @summary Checks the user answer
   * @memberof mathex.TextSelectFieldStep.prototype
   * @method checkSelectFieldResult
   * @param {Object} field The mootools select input object
   * @return void
   */
  this.checkSelectFieldResult = function(field) {
    this.removeInputEvents();

    if(this.end_message) {
      var callback = function() {
        mathex.Shared.showMessage(this.end_message, 'message', null);
      }.bind(this)
    }
    else {
      var callback = null;
    }

    if(!(field.value == this.result || this.errors == 1)) {
      mathex.Shared.showMessage('Risposta errata, riprova', 'error', this.addInputEvents.bind(this));
      field.set('value', '');
      this.errors = 1;
    }
    else {

      if(field.value == this.result) {
        mathex.Shared.showMessage('Risposta esatta', 'success', callback);
      }
      else {
        mathex.Shared.showMessage('Risposta errata.', 'failed', callback);
        field.set('value', this.result);
      }
      this.deactivate();
      this.router.endStep();
    }

  }
  /**
   * @summary Adds events to the select input
   * @memberof mathex.TextSelectFieldStep.prototype
   * @method addInputEvents
   * @return void
   */
  this.addInputEvents = function() {
    var self = this;
    document.id('select_' + this.string).addEvent('change', self.changehandler = function(evt) {
        self.checkSelectFieldResult(document.id('select_' + this.string));
      }.bind(this));
  }
  /**
   * @summary Removes events from the select input
   * @memberof mathex.TextSelectFieldStep.prototype
   * @method removeInputEvents
   * @return void
   */
  this.removeInputEvents = function() {
    var self = this;
    document.id('select_' + this.string).removeEvent('change', self.changehandler);
  }
  /**
   * @summary Deactivates all inputs
   * @memberof mathex.TextSelectFieldStep.prototype
   * @method deactivate
   * @return void
   */
  this.deactivate = function() {
    document.id('select_' + this.string).removeEvents('change');
    document.id('select_' + this.string).setProperty('readonly', 'readonly').setProperty('disabled', 'disabled');
  }
}
mathex.TextSelectFieldStep.prototype = mathex.Step;

/**
 * @summary Exercises - One already rendered text input field activation
 * @memberof mathex
 * @constructs mathex.FieldStep
 * @extends mathex.Step
 * @param {String} input_id The input identifier
 * @param {Mixed} result The correct result
 * @param {String} [end_message] A message to be displayed at the end of the step
 * @param {Object} options The step options
 * @param {Boolean} options.type The result type cast
 * @return {Object} FieldStep instance
 * @example
 *  // if a previous defined (in a mathex.TextFieldStep object) input field exists, with id=2
 *  var step = new mathex.FieldStep(2, 16, null, {type: 'int'});
 */
mathex.FieldStep = function(input_id, result, end_message, options) {

  this.input_id = input_id;
  this.result = result;
  this.options = options;
  this.end_message = typeof end_message == 'undefined' ? null : end_message;
  /**
   * @summary Executes the step
   * @description Activates the text input
   * @memberof mathex.FieldStep.prototype
   * @method run
   * @param {Object} router a mathex.Router instance
   * @return void
   */
  this.run = function(router) {
    var self = this;
    this.router = router;
    document.id('field_' + this.input_id).removeProperty('readonly').removeClass('disabled');
    self.addInputEvents();
  }
  /**
   * @summary Executes the step
   * @description Activates the input
   * @memberof mathex.FieldStep.prototype
   * @method addInputEvents
   * @return void
   */
  this.addInputEvents = function() {
    var self = this;
    var input_obj = document.id('field_' + this.input_id);
    input_obj.addEvent('keydown', this.keyhandler = function(evt) {
      if(evt.key == 'enter') {
        self.checkFieldResult(input_obj, self.result, typeof self.options != 'undefined' ? self.options: null);
      }
    })
  }
  /**
   * @summary Removes events from the text input
   * @memberof mathex.FieldStep.prototype
   * @method removeInputEvents
   * @return void
   */
  this.removeInputEvents = function() {
    var input_obj = document.id('field_' + this.input_id);
    input_obj.removeEvent('keydown', this.keyhandler);
  }
  /**
   * @summary Deactivates all inputs
   * @memberof mathex.FieldStep.prototype
   * @method deactivate
   * @return void
   */
  this.deactivate = function() {
    var input_obj = document.id('field_' + this.input_id);
    input_obj.removeEvents('keydown');
    input_obj.setProperty('readonly', 'readonly');
  }

}
mathex.FieldStep.prototype = mathex.Step;

/**
 * @summary Exercises - Only text step
 * @memberof mathex
 * @constructs mathex.TextStep
 * @extends mathex.Step
 * @param {String} tpl The step template.<br />
 *                        <p>The math to be parsed by mathjax must be placed inside the tag {%%}, i.e. {% 2^4=16 %}</p>
 * @param {String} [end_message] A message to be displayed at the end of the step
 * @param {Object} [options] The step options
 * @param {Boolean} [options.container=true] Whether or not to insert the exercise text inside a div container
 * @return {Object} TextStep instance
 * @example
 *  var step = new mathex.TextStep('<p>my text</p>', 'my message', {container: false});
 */
mathex.TextStep = function(tpl, end_message, options) {

  this.container = options && typeof options.container != 'undefined' ? options.container : true;

  this.tpl = mathex.Shared.parseTpl(tpl, {});
  this.end_message = typeof end_message == 'undefined' ? null : end_message;
  /**
   * @summary Executes the step
   * @description Renders the parsed text
   * @memberof mathex.TextStep.prototype
   * @method run
   * @param {Object} router a mathex.Router instance
   * @return void
   */
  this.run = function(router) {
    var self = this;
    this.router = router;
    if(this.container) {
      var div = new Element('div').set('html', this.tpl).inject($('container'), 'bottom');
    }
    else {
      $('container').set('html', $('container').get('html') + this.tpl);
    }
    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);
    if(this.end_message) {
      mathex.Shared.showMessage(this.end_message, 'message', null);
    }
    this.router.endStep();

  };

}
mathex.TextStep.prototype = mathex.Step;

/***********************************************************************
 *
 *  QUESTIONS
 *  Set of questions proposed one after the other, with rating and end message
 *
 ***********************************************************************/
/**
 * @summary Questions - Question class
 * @memberof mathex
 * @constructs mathex.Question
 * @param {Object} prop Properties object
 * @param {String} prop.text The question text
 *                              <p>The math to be parsed by mathjax must be placed inside the tag {%%}, i.e. {% 2^4=16 %}</p>
 * @param {Array} prop.answers Proposed answers. Each answer can contain mathjsx math.
 * @param {Number} prop.correct_answer Index of the correct answer
 * @return {Object} mathex.Question instance
 * @example 
 *  var question = new mathex.Question({
 *   text: '2. Which of the following is an identity?',
 *   answers: [
 *     '{% 3x = 9 %}',
 *     '{% 2a = 5 %}',
 *     '{% 7 + 8 = 16 %}',
 *     '{% 15 - 9 = 6 %}'
 *   ],
 *   correct_answer: 3
 * })
 */
mathex.Question = function(prop) {

  this.text = prop.text;
  this.answers = prop.answers;
  this.correct_answer = prop.correct_answer;
  this.errors = 0;
  this.last = false;
  /**
   * @summary Sets the last property to true
   * @memberof mathex.Question.prototype
   * @method setLast
   * @return void
   */
  this.setLast = function() {
    this.last = true;
  }
  /**
   * @summary Executes the question
   * @description Renders the question activating the answers
   * @memberof mathex.Question.prototype
   * @method run
   * @param {Object} router The mathex.QuestionRouter instance
   * @return void
   */
  this.run = function(router) {
    $('answers_container').empty();
    this.router = router;
    var self = this;
    this.text = mathex.Shared.parseTpl(this.text, []);
    var div = new Element('div').set('html', this.text).inject($('answers_container'), 'bottom');
    this.list = new Element('ul.test');
    for(var i = 0; i < this.answers.length; i++) {
      var answer = this.answers[i];
      var answer_text = new Element('label[for=answ' + i + ']').set('html', mathex.Shared.parseTpl(answer, []));
      var line = new Element('li');
      var input = new Element('input#answ' + i + '[type=radio][name=answer][value=' + i +']').addEvent('click', function() {
        self.checkAnswer(this.get('value'), self.correct_answer);
      });
      line.adopt(input, answer_text).inject(this.list);
    }
    this.list.inject($('answers_container'), 'bottom');
    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);
  };
  /**
   * @summary Checks the user answer
   * @description Checks the answer, updates the gui, saves the rating and shows a response message to the user
   * @memberof mathex.Question.prototype
   * @method checkAnswer
   * @param {Number} index Index of the choosen answer
   * @param {Number} correct Index of the correct answer
   * @return void
   */
  this.checkAnswer = function(index, correct) {

    if(!(index == correct || this.errors == 1)) {
      mathex.Shared.showMessage('Risposta errata, riprova', 'error');
      var x = new Element('span.x').set('html', '×').inject(this.list.getElements('input')[index].setStyle('display', 'none'), 'before');
      this.errors = 1;
    }
    else {

      if(this.last) {
        var callback = function() {
          mathex.Shared.showMessage(this.router.getEndMessage(), 'message', null);
        }.bind(this)
      }
      else {
        var callback = function() {};
      }

      if(index == correct) {
        if(this.errors == 1) {
          var result = 'sattempt';
          this.router.addPoint(0.5);
        }
        else {
          var result = 'success';
          this.router.addPoint(1);
        }
        mathex.Shared.showMessage('Risposta esatta', 'success', function() {this.router.endStep(result, this); callback(); }.bind(this));
        var v = new Element('span.v').set('html', '✔').replaces(this.list.getElements('input')[index]);
      }
      else {
        mathex.Shared.showMessage('Risposta errata', 'failed', function() {this.router.endStep('failed', this); callback(); }.bind(this));
        var x = new Element('span.x').set('html', '×').inject(this.list.getElements('input')[index].setStyle('display', 'none'), 'before');
        var v = new Element('span.v').set('html', '✔').replaces(this.list.getElements('input')[correct]);
      }
    }
  };
  /**
   * @summary Removes events from inputs
   * @memberof mathex.Question.prototype
   * @method removeEvents
   * @return void
   */
  this.removeEvents = function() {
    this.list.getElements('input').setProperty('disabled', 'disabled').removeEvents('click');
  };
}

/**
 * @summary Questions - Question Router Class
 * @classdesc The Router class handles the navigation through questions
 * @constructs mathex.QuestionRouter
 * @memberof mathex
 * @return {Object} mathex.QuestionRouter instance
 */
mathex.QuestionRouter = function() {
  /**
   * @summary Initializes the QuestionRouter instance
   * @description Renders widgets and gui
   * @memberof mathex.QuestionRouter.prototype
   * @method init
   * @param {Array} s Array of managed questions @see mathex.Question
   * @return void
   */
  this.init = function(s) {
    // widgets
    if(mathex.config.font_ctrl) {
      mathex.Shared.fontWidget();
    }
    mathex.Shared.calculatorWidget();
    this.steps = s;
    this.steps[s.length - 1].setLast();
    var answer_div = new Element('div#answers_container').inject($('container'), 'bottom');
    var nav_div = new Element('div#answers_nav').inject($('container'), 'bottom');
    for(var i = 1, l = s.length; i < l + 1; i++) {
      var navel = new Element('span#nav' + i).set('text', i).inject(nav_div);
    }
    this.points = 0;
  };
  /**
   * @summary Add a point to the rating
   * @memberof mathex.QuestionRouter.prototype
   * @method addPoint
   * @param {Number} point The point to add
   * @return void
   */
  this.addPoint = function(point) {
    this.points += point;
  };
  /**
   * @summary Executes the given question
   * @memberof mathex.QuestionRouter.prototype
   * @method startStep
   * @param {Number} [index=0] The index of the question to execute
   * @return void
   */
  this.startStep = function(index) {
    index = index ? index: 0;
    $('nav' + (index + 1)).set('class', 'current');
    mathex.QuestionRouter.prototype.startStep.call(this, index);
  };
  /**
   * @summary Ends the question step
   * @memberof mathex.QuestionRouter.prototype
   * @method endStep
   * @param {String} result The question result ('success', 'failed')
   * @param {Object} obj The mathex.Question object
   * @return void
   */
  this.endStep = function(result, obj) {
    $('nav' + (this.getCurrent() + 1)).set('class', result);
    mathex.QuestionRouter.prototype.endStep.call(this, obj.removeEvents.bind(obj));
  };
  /**
   * @summary Gets the final message basing upon the final rating
   * @memberof mathex.QuestionRouter.prototype
   * @method getEndMessage
   * @return {String} The message
   */
  this.getEndMessage = function() {
    var points = Math.floor(this.points);
    var message = "Il tuo punteggio è " + points + '.\n';
    if(points < 4) {
      message += "Numerosissime lacune.\nRistudia tutto l'argomento.";
    }
    else if(points < 6) {
      message += "La preparazione è lacunosa.\nRipassa tutto l'argomento.";
    }
    else if(points == 6) {
      message += "La prova evidenzia alcune lacune da colmare con esercizi di recupero.";
    }
    else if(points < 9) {
      message += "Nel complesso la preparazione è completa, puoi migliorare con esercizi di potenziamento.";
    }
    else {
      message += "La prova dimostra una preparazione adeguata.";
    }

    return message;
  };

}
mathex.QuestionRouter.prototype = new mathex.Router();

/***********************************************************************
 *
 *  FAQ
 *  Faq have an index and can be browsed
 *
 ***********************************************************************/
/**
 * @summary FAQ - Faq Class
 * @classdesc Stores an array containing all the faq items
 * @constructs mathex.Faq
 * @memberof mathex
 * @param {Array} items The array describing all items. Each item is an object with properties:
 *                      <ul>
 *                        <li><b>question</b>: string. The question, can contain mathjax math inside the tag {% LATEX MATH HERE %}</li>
 *                        <li><b>answer</b>: string. The answer, can contain mathjax math inside the tag {% LATEX MATH HERE %}, and links to other faqs, 
 *                            whether indexed faqs or not (in this case showing the linked one in a layer): {{link_text:3}} or {{link_text:3:layer}}</li>
 *                        <li><b>audio</b>: object. The audio object for the question and answer, it has the 'mp3' and 'ogg' properties storing the files' paths</li>
 *                      </ul>
 * @return {Object} mathex.Faq instance
 * @example
 *  var faq = new mathex.Faq([
 *    {
 *      question: "Which color is yellow?",
 *         answer: 'yellow!',
 *         audio: {
 *           mp3: 'audio/myfile.mp3',
 *           ogg: 'audio/myfile.ogg',
 *      }
 *    },
 *    {
 *      question: "How much it is {% 1 + 1 %}?",
 *      answer: "<p>TWO!</p>"
 *    }
 *  ]);
 */
mathex.Faq = function(items) {
  this.items = items;
}
/**
 * @summary FAQ - Faq Router Class
 * @classdesc handles the faq navigation and rendering
 * @memberof mathex
 * @constructs mathex.FaqRouter
 * @param {Object} faq The mathex.Faq instance
 * @return {Object} mathex.Faq instance
 */
mathex.FaqRouter = function(faq) {
  this.faq = faq;
  /**
   * @summary Starts the execution of the faq
   * @memberof mathex.FaqRouter.prototype
   * @method start
   * @return void
   */
  this.start = function() {
    this.faq_div = new Element('div#faq_container').inject($('container'), 'bottom');
    this.faq_nav = new Element('div#faq_nav').inject($('container'), 'bottom');
    // widgets
    if(mathex.config.font_ctrl) {
      mathex.Shared.fontWidget();
    }
    mathex.Shared.calculatorWidget();
    this.renderIndex();
  }
  /**
   * @summary Renders the faq index
   * @memberof mathex.FaqRouter.prototype
   * @method renderIndex
   * @return void
   */
  this.renderIndex = function() {
    this.faq_div.empty();
    this.faq_nav.empty();
    var list = new Element('ul').inject(this.faq_div);
    this.faq.items.each(function(item, index) {
      if(typeof item.index == 'undefined' || item.index) {
        var li = new Element('li.link_faq')
          .set('html', mathex.Shared.parseTpl(item.question, []))
          .addEvent('click', function() {
            this.renderFaq(index);
          }.bind(this))
          .inject(list, 'bottom');
      }
    }.bind(this));

    mathex.Shared.playerWidget(null);

    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);
  }
  /**
   * @summary Renders one single faq (question, answer)
   * @memberof mathex.FaqRouter.prototype
   * @method renderFaq
   * @param {Number} index the index of the faq to be rendered
   * @return void
   */
  this.renderFaq = function(index) {

    index = parseInt(index);
    /* index */
    var self = this;
    var title = new Element('h3.select').set('html', mathex.Shared.parseTpl(this.faq.items[index].question, []))
      .addEvent('click', function() {
        var list = $(this).getNext('ul');
        if(list.getStyle('display') == 'none') {
          list.setStyle('display', 'inline-block');
        }
        else {
          list.setStyle('display', 'none');
        }
      });
    var fake_select = new Element('ul.select');
    this.faq.items.each(function(item, opt_index) {
      if((typeof item.index == 'undefined' || item.index) && opt_index != index) {
        var option = new Element('li')
          .set('html', mathex.Shared.parseTpl(item.question, []))
          .addClass('link')
          .setProperty('data-value', opt_index)
          .addEvent('click', function() {
            self.renderFaq($(this).get('data-value'));
            window.location.hash = 'top';
          })
          .inject(fake_select);
      }
    }.bind(this));

    var item = this.faq.items[index];
    this.faq_div.empty();
    this.faq_nav.empty();
    var answer = mathex.Shared.parseTpl(item.answer, []);
    var link_rexp = new RegExp("{{\s*(.*?):([0-9]+):?(layer)?\s*}}", "gim");
    var answer = answer.replace(link_rexp, "<span class=\"link\" onclick=\"router.goto($2, '$3')\">$1</span>");
    this.faq_div.adopt(title, fake_select, new Element('div').set('html', answer));

    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);

    var prev = null, next = null;
    if(index > 0) {
      var prev_exists = false;
      for(var i = index - 1; i >= 0; i--) {
        if(typeof this.faq.items[i].index == 'undefined' || this.faq.items[i].index) {
          prev_exists = true;
        }
      }
      if(prev_exists) {
        prev = new Element('span').set('text', 'precedente').addEvent('click', function() {
          this.renderFaq(index - 1);
          window.location.hash = 'top';
        }.bind(this)).inject(this.faq_nav);
      }
    }
    var toindex = new Element('span').set('text', 'indice').addEvent('click', function() {
      this.renderIndex();
      window.location.hash = 'top';
    }.bind(this)).inject(this.faq_nav);

    if(index < this.faq.items.length - 1) {
      var next_exists = false;
      for(var i = index + 1, l = this.faq.items.length; i < l; i++) {
        if(typeof this.faq.items[i].index == 'undefined' || this.faq.items[i].index) {
          next_exists = true;
        }
      }
      if(next_exists) {
        next = new Element('span').set('text', 'successiva').addEvent('click', function() {
          this.renderFaq(index + 1);
          window.location.hash = 'top';
        }.bind(this)).inject(this.faq_nav);
      }
    }

    /* audio */
    if(typeof item.audio != 'undefined') {
      mathex.Shared.playerWidget(item.audio);
    }
    else {
      mathex.Shared.playerWidget(null);
    }

  };
  /**
   * @summary Moves to another faq
   * @memberof mathex.FaqRouter.prototype
   * @method goto
   * @param {Number} index The index of the faq to go to
   * @param {String} [layer] Whether to show the faq over a layer (if its value is 'layer') or not
   * @return void
   */
  this.goto = function(index, layer) {
    if(layer == 'layer') {
      var item = this.faq.items[index];
      var answer = mathex.Shared.parseTpl(item.answer, []);
      var link_rexp = new RegExp("{{\s*(.*?):([0-9]+):?(layer)?\s*}}", "gim");
      var answer = answer.replace(link_rexp, "<span class=\"link\" onclick=\"router.goto($2, '$3')\">$1</span>");
      var viewport = mathex.Shared.getViewport();
      var layer = new Element('div.layer').setStyles({
        'position': 'absolute',
        'margin': 'auto',
        'top': 0,
        'bottom': 0,
        'left': 0,
        'right': 0,
        'width': '500px',
        'height': '50%',
        'overflow': 'auto'
        //'top': (viewport.cY - 100) + 'px',
        //'left': (viewport.cX - 200) + 'px',
      }).inject($(document.body));
      var title = new Element('h2').set('html', mathex.Shared.parseTpl(item.question, []));
      var close = new Element('div.link.button-close').setStyles({
        position: 'absolute',
        right: 0,
        top: 0,
        'font-size': '32px'
      }).set('html', '×').addEvent('click', function() { layer.dispose(); });
      layer.adopt(close, title, new Element('div').set('html', answer));
    }
    else {
      this.renderFaq(index);
    }
  }
}

/***********************************************************************
 *
 *  Recovery exercises
 *  Index with questions. Each question may contain text, togglable images and initially hidden boxes which are shown when clicking over them
 *
 ***********************************************************************/
/**
 * @summary Recovery - Recovery Class
 * @classdesc Stores an array containing all the recovery items
 * @memberof mathex
 * @constructs mathex.Recovery
 * @param {Array} items The array describing all items. Each item is an object with properties:
 *                      <ul>
 *                        <li><b>title</b>: string. The item title (for the index), can contain mathjax math inside the tag {% LATEX MATH HERE %}</li>
 *                        <li><b>tpl</b>: string. The item tpl, can contain mathjax math inside the tag {% LATEX MATH HERE %}. Initially hidden boxes shown at mouse click must be in the form: [[x]]</li>
 *                      </ul>
 * @return {Object} mathex.Recovery instance
 * @example
 *  var recovery = new mathex.Recovery([
 *    {
 *      title: "Exercises page 3",
 *      tpl: '<img class="toggle" src="img/img.png" width="200" />' +
 *           '<img class="toggle" src="img/img.png" />' +
 *           '<p>{% 1 + 1 %} = [[2]]</p>' +
 *           '<p>{% 2 + 1 %} = [[3]]</p>'
 *    },
 *    {
 *      title: "Problem",
 *      tpl: "<p>Bla bla </p>{% 3x -5 = x + 7 %}<p>Meow meow...</p>"
 *    }
 *  ]);
 */
mathex.Recovery = function(items) {
  this.items = items;
}
/**
 * @summary Recovery - Recovery Router Class
 * @classdesc handles the recovery navigation and rendering
 * @constructs mathex.RecoveryRouter
 * @memberof mathex
 * @param {Object} recovery The mathex.Recovery instance
 * @return {Object} mathex.RecoveryRouter instance
 */
mathex.RecoveryRouter = function(recovery) {

  this.recovery = recovery;
  /**
   * @summary Starts the execution of the recovery
   * @memberof mathex.RecoveryRouter.prototype
   * @method start
   * @return void
   */
  this.start = function() {
    this.r_div = new Element('div#r_container').inject($('container'), 'bottom');
    this.r_nav = new Element('div#r_nav').inject($('container'), 'bottom');
    // widgets
    if(mathex.config.font_ctrl) {
      mathex.Shared.fontWidget();
    }
    mathex.Shared.calculatorWidget();
    this.renderIndex();
  }
  /**
   * @summary Renders the recovery index
   * @memberof mathex.RecoveryRouter.prototype
   * @method renderIndex
   * @return void
   */
  this.renderIndex = function() {
    window.location.hash = '';
    this.r_div.empty();
    this.r_nav.empty();
    var list = new Element('ul').inject(this.r_div);
    this.recovery.items.each(function(item, index) {
      var li = new Element('li.link')
        .set('html', mathex.Shared.parseTpl(item.title, []))
        .addEvent('click', function() {
          this.renderRecovery(index);
        }.bind(this))
        .inject(list, 'bottom');
    }.bind(this));

    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);
  }
  /**
   * @summary Renders one recovery item
   * @memberof mathex.RecoveryRouter.prototype
   * @method renderRecovery
   * @param {Number} index the index of the item to be rendered
   * @return void
   */
  this.renderRecovery = function(index) {
    window.location.hash = '';
    var item = this.recovery.items[index];
    this.r_div.empty();
    this.r_nav.empty();
    var tpl = mathex.Shared.parseTpl(item.tpl, []);

    // parse for hidden results
    var hidden_rexp = new RegExp("\\[\\[(.*?)\\]\\]", "gim");
    tpl = tpl.replace(hidden_rexp, "<span class=\"recovery-hidden\">$1</span>");
    this.r_div.set('html', "<h2>" + mathex.Shared.parseTpl(item.title, []) + "</h2>" + tpl);

    document.getElements('.recovery-hidden').addEvent('click', function() {
      this.removeClass('recovery-hidden');
    })

    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);

    var prev = null, next = null;
    if(index > 0) {
      prev = new Element('span').set('text', 'precedente').addEvent('click', function() {
        this.renderRecovery(index - 1);
        window.location.hash = 'top';
      }.bind(this)).inject(this.r_nav);
    }
    var toindex = new Element('span').set('text', 'indice').addEvent('click', function() {
      this.renderIndex();
      window.location.hash = 'top';
    }.bind(this)).inject(this.r_nav);

    if(index < this.recovery.items.length - 1) {
      next = new Element('span').set('text', 'successiva').addEvent('click', function() {
        this.renderRecovery(index + 1);
        window.location.hash = 'top';
      }.bind(this)).inject(this.r_nav);
    }
  };
  /**
   * @summary Moves to another item
   * @memberof mathex.RecoveryRouter.prototype
   * @method goto
   * @param {Number} index The index of the item to go to
   * @return void
   */
  this.goto = function(index) {
    this.renderRecovery(index);
  }
}

/***********************************************************************
 *
 *  Test
 *  Test with questions and rating
 *
 ***********************************************************************/
/**
 * @summary Test - Test Question Factory Class
 * @classdesc returns a Test question specific instance
 * @memberof mathex
 * @constructs mathex.TestQuestion
 * @param {String} type Type of the question to create
 * @param {Object} options Object to be passed to the specific question class constructor
 * @return {Object} A specific test question instance
 */
mathex.TestQuestion = function(type, options) {
  if(type == 'input') {
    return new mathex.TestInputQuestion(options);
  }
  else if(type == 'radio') {
    return new mathex.TestRadioQuestion(options);
  }
  else return null;
}
/**
 * @summary Test - Test question, answers with only text fields
 * @memberof mathex
 * @constructs mathex.TestInputQuestion
 * @param {Object} [options] Options
 * @param {String} [options.question] The question and answer template to be parsed.
 *                                    <p>The math to be parsed by mathjax (latex syntax) must be placed inside the tag {%%}, i.e. {% 2^4=16 %}</p>
 *                                    <p>The input fields inside the math must be formatted this way: \\FormInput2, where 2 is the id of the input which is described through the inputs parameter.</p>
 * @param {String} [options.inputs] Object storing ll the inputs description. Each input object has the properties:
 *                                  <ul>
 *                                    <li><b>size</b>: number. The input field size</li>
 *                                    <li><b>result</b>: string. The input field result</li>
 *                                    <li><b>type</b>: string. The input field result type ('float', 'int', 'string_case')</li>
 *                                  </ul>
 * @return {Object} mathex.TestInputQuestion instance
 * @example
 *  var question1 = new mathex.TestQuestion('input', {
 *    question: '<p>How do you write the pow with base 20 and exponent 3?</p><p>{%\\FormInput0 ^ \\FormInput1%}</p>',
 *    inputs: {
 *      0: {
 *        size: 2,
 *        result: '20',
 *        type: 'int'
 *      },
 *      1: {
 *        size: 1,
 *        result: 3,
 *        type: 'int'
 *      }
 *    }
 *  });
 */
mathex.TestInputQuestion = function(options) {

  this.question = options.question;
  this.inputs = options.inputs;
  /**
   * @summary Executes the test question
   * @memberof mathex.TestInputQuestion.prototype
   * @method run
   * @param {Object} test The mathex.Test instance
   * @return void
   */
  this.run = function(test) {
    var self = this;
    this.tpl = mathex.Shared.parseTpl(this.question, this.inputs);
    this.test = test;
    var div = new Element('div').set('html', this.tpl).inject($('container'), 'bottom');
    var confirm = new Element('input[type=button][value=conferma]')
      .addEvent('click', self.checkAnswer.bind(self))
      .inject(new Element('div').inject($('container'), 'bottom'));
    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);
  }
  /**
   * @summary Cheks the user answer, saves the result and proceeds with the next question
   * @memberof mathex.TestInputQuestion.prototype
   * @method checkAnswer
   * @return void
   */
  this.checkAnswer = function() {
    var result = true;
    Object.each(this.inputs, function(input, index) {
      var value = $('field_' + index).get('value');
      result = result && mathex.Shared.checkResult(input.type, input.result, value);
    }.bind(this));

    this.test.saveResult(result);
    this.test.nextQuestion();

  }
}
/**
 * @summary Test - Test radio question, answer with one multiple radio choice
 * @memberof mathex
 * @constructs mathex.TestRadioQuestion
 * @param {Object} options Options
 * @param {String} options.question The question and answer template to be parsed
 *                                    <p>The math to be parsed by mathjax (latex syntax) must be placed inside the tag {%%}, i.e. {% 2^4=16 %}</p>
 *                                    <p>The radio choices must be written this way: [[1]] choice, where 1 is the index of the choice.</p>
 * @param {Number} options.result The index of the correct answer
 * @return {Object} mathex.TestRadioQuestion instance
 * @example
 *  var question3 = new mathex.TestQuestion('radio', {
 *    question: 'Which equation is wrong?' + 
 *      '<ul>' +
 *      '<li>[[0]] {% 5^3 * 5^4 = 5^7 %}</li>' +
 *      '<li>[[1]] {% 2^5 * 3^5 = 6^5 %}</li>' +
 *      '<li>[[2]] {% 9^6 + 9^2 = 9^8 %}</li>' +
 *      '<li>[[3]] {% 7^8 : 7 = 7^7 %}</li>' +
 *      '</ul>',
 *    result: 2,
 *  });
 *
 */
mathex.TestRadioQuestion = function(options) {

  this.question = options.question;
  this.result = options.result;
  /**
   * @summary Executes the test question
   * @memberof mathex.TestRadioQuestion.prototype
   * @method run
   * @param {Object} test The mathex.Test instance
   * @return void
   */
  this.run = function(test) {
    var self = this;
    this.string = String.uniqueID();
    this.tpl = mathex.Shared.parseTpl(this.question, {});
    var radio_rexp = new RegExp("\\[\\[([0-9]*?)\\]\\]", "gim");
    this.tpl = this.tpl.replace(radio_rexp, "<input type=\"radio\" name=\"radio_" + this.string + "\" id=\"radio_$1\" />");
    this.test = test;
    var div = new Element('div').set('html', this.tpl).inject($('container'), 'bottom');
    var confirm = new Element('input[type=button][value=conferma]')
      .addEvent('click', self.checkAnswer.bind(self))
      .inject(new Element('div').inject($('container'), 'bottom'));
    MathJax.Hub.Queue(['Typeset',MathJax.Hub]);
  }
  /**
   * @summary Cheks the user answer, saves the result and proceeds with the next question
   * @memberof mathex.TestRadioQuestion.prototype
   * @method checkAnswer
   * @return void
   */
  this.checkAnswer = function() {
    var result = false;
    $$('input[type=radio]').each(function(radio, index) {
      if(radio.checked && index == this.result) {
        result = true;
      };
    }.bind(this));

    this.test.saveResult(result);
    this.test.nextQuestion();

  }

}
/**
 * @summary Test - Test class
 * @classdesc Handles the test rendering and flow
 * @constructs mathex.Test
 * @memberof mathex
 * @return {Object} mathex.Test instance
 */
mathex.Test = function() {

  this.questions = [];
  this.current = 0;
  this.results = [];
  /**
   * @summary Initializes the Test instance
   * @memberof mathex.Test.prototype
   * @method init
   * @param {Array} questions Array of test question objects
   * @param {Object} options Options
   * @param {Array} options.steps The steps used for the rating, in asc order, check is made this way: if result <= step sup limit
   * @param {Array} options.rating Array of objects describing the rating of the steps previously defined. Each object has a message property (text to be shown) and a color property (text color)
   * @param {Boolean} [options.widgets=false] Whether or not to create the widgets @see mathex.Shared
   * @return void
   * @example
   *  test.init([question1, question2, question3], {
   *    steps: [2, 7, 10],
   *    rating: [
   *      {message: 'very bad', color: 'red'},
   *      {message: 'quite good', color: 'yellow'},
   *      {message: 'meow', color: 'green'},
   *    ]
   *  });
   *
   **/
  this.init = function(questions, options) {
    this.options = options;
    if(options && typeof this.options.widgets != 'undefined') {
      // widgets
      if(mathex.config.font_ctrl) {
        mathex.Shared.fontWidget();
      }
      mathex.Shared.calculatorWidget();
    }
    this.questions = questions;
  }
  /**
   * @summary Executes the given index question
   * @memberof mathex.Test.prototype
   * @method start
   * @params {Number} [index=0] The index of the question to execute
   * @return void
   */
  this.start = function(index) {
    $('container').empty();
    index = typeof index != 'undefined' ? index : 0;
    try {
      var question = this.questions[index];
      this.current = index;
      question.run(this);
    }
    catch(err) {
      console.log(err);
      console.log('question undefined or not a question');
    }
  }
  /**
   * @summary Stores the given result
   * @memberof mathex.Test.prototype
   * @method saveResult
   * @params {Boolean} result The result to store
   * @return void
   */
  this.saveResult = function(result) {
    this.results.push(result ? 1 : 0);
  }
  /**
   * @summary Goes to the next questions or the end of the test
   * @memberof mathex.Test.prototype
   * @method nextQuestion
   * @return void
   */
  this.nextQuestion = function() {
    if(this.current == this.questions.length - 1) {
      this.renderResults();
    }
    else {
      this.start(this.current + 1);
    }
  }
  /**
   * @summary Renders the test final rating
   * @memberof mathex.Test.prototype
   * @method renderResults
   * @return void
   */
  this.renderResults = function() {
    var table = new Element('table.test-result');
    var tr1 = new Element('tr').inject(table);
    var tr2 = new Element('tr').inject(table);
    for(var i = 0, l = this.questions.length; i < l; i++) {
      tr1.adopt(new Element('th').set('text', 'Quesito ' + (i + 1)));
      tr2.adopt(new Element('td').set('text', this.results[i]));
    }
    tr1.adopt(new Element('th').set('text', 'Totale'));
    var total = this.results.reduce(function(previousValue, currentValue, index, array){ return previousValue + currentValue; });
    tr2.adopt(new Element('td').set('text', total));

    // message
    for(var i = 0, l = this.options.steps.length; i < l; i++) {
      var limit = this.options.steps[i];
      if(total <= limit) {
        var rating = this.options.rating[i];
        break;
      }
    }
    var rating_element = new Element('p.test-rating').setStyle('color', rating.color).set('text', rating.message);

    $('container').empty();
    $('container').adopt(new Element('p').set('text', 'Risultati'), table, rating_element);
  }
}

/**
 * @summary Attaches a load event to the window object which creates a top anchor
 * @event
 */
window.addEvent('load', function() {
    var anchor = new Element('a', {name: 'top'}).set('text', 'top').setStyles({
        color: '#fff',
        'line-height': 0,
        position: 'absolute',
        top: 0
    }).inject($$('header')[0], 'after');
});