File:  [LON-CAPA] / modules / damieng / graphical_editor / loncapa_daxe / web / nodes / tex_mathjax.dart
Revision 1.8: download - view: text, annotated - select for diffs
Tue Jan 17 22:10:08 2017 UTC (7 years, 4 months ago) by damieng
Branches: MAIN
CVS tags: HEAD
better equation display with Perl functions

/*
  This file is part of LONCAPA-Daxe.

  LONCAPA-Daxe is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  LONCAPA-Daxe is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with Daxe.  If not, see <http://www.gnu.org/licenses/>.
*/

part of loncapa_daxe;

/**
 * Displays tm and dtm elements (LaTeX inline and display math) with MathJax.
 * Note: conversion from m to tm/dtm has been delayed. This is now used
 * to display m.
 * Jaxe display type: 'texmathjax'.
 */
class TeXMathJax extends DaxeNode {
  
  TeXMathJax.fromRef(x.Element elementRef) : super.fromRef(elementRef) {
  }
  
  TeXMathJax.fromNode(x.Node node, DaxeNode parent) : super.fromNode(node, parent) {
  }
  
  @override
  h.Element html() {
    h.SpanElement span = new h.SpanElement();
    span.id = "$id";
    span.classes.add('dn');
    if (!valid)
      span.classes.add('invalid');
    span.classes.add('tex');
    span.onClick.listen((h.MouseEvent event) => editDialog());
    return(span);
  }
  
  @override
  void newNodeCreationUI(ActionFunction okfct) {
    editDialog(() => okfct());
  }
  
  @override
  Position firstCursorPositionInside() {
    return(null);
  }
  
  @override
  Position lastCursorPositionInside() {
    return(null);
  }
  
  @override
  void afterInsert() {
    updateEquationDisplay();
  }
  
  void updateEquationDisplay() {
    String equationText = '?';
    if (firstChild != null && firstChild.nodeValue.trim() != '' &&
        firstChild.nodeValue.trim() != '\$\$')
      equationText = firstChild.nodeValue;
    h.SpanElement span = h.document.getElementById(id);
    if (span == null)
      return;
    equationText = convertForMathJax(equationText, getAttribute('eval') == 'on');
    span.text = equationText;
    js.JsArray params = new js.JsObject.jsify( ['Typeset', js.context['MathJax']['Hub'], id] );
    js.context['MathJax']['Hub'].callMethod('Queue', [params]);
    _fixCursorPosition();
  }
  
  /**
   * Move the cursor if it is inside the element.
   */
  void _fixCursorPosition() {
    Position start = page.getSelectionStart();
    if (start != null) {
      DaxeNode dn = start.dn;
      if (dn is DNText)
        dn = dn.parent;
      if (dn == this) {
        page.moveCursorTo(new Position(parent, parent.offsetOf(this) + 1));
        page.updateAfterPathChange();
      }
    }
  }
  
  void editDialog([ActionFunction okfct]) {
    TeXMathJaxeDialog dlg = new TeXMathJaxeDialog(this, okfct);
    dlg.show();
  }
  
  /**
   * Returns a new TeX expression ready to be displayed by MathJax
   * with a nice display of variables and functions.
   */
  String convertForMathJax(String text, bool eval) {
    bool hasDelimiters = false;
    text = text.trim();
    if (text.length > 3 && text.startsWith('\$\$') && text.endsWith('\$\$')) {
      text = "\\[${text.substring(2,text.length-2)}\\]";
      hasDelimiters = true;
    } else if (text.length > 1 && text.startsWith('\$') && text.endsWith('\$')) {
      text = "\\(${text.substring(1,text.length-1)}\\)";
      hasDelimiters = true;
    }
    if (hasDelimiters && eval) {
      // turn Perl variables to tt style
      text = text.replaceAllMapped(new RegExp(r'\$[a-zA-Z_]*[0-9]*(\[[^\]]*\])?'), (match) {
        String name = match.group(0);
        name = name.replaceAll('_', '\\_');
        return "\\mathtt{$name}";
      });
      // same for Perl functions
      text = text.replaceAllMapped(new RegExp(r'&[a-zA-Z_]*[0-9]*\([^)]*\)'), (match) {
        String fct = match.group(0);
        fct = fct.replaceAll('&', '\\&');
        fct = fct.replaceAll('_', '\\_');
        fct = fct.replaceAll("'", "\\textsf{'}");
        return "\\mathtt{$fct}";
      });
    }
    return(text);
  }
  
  @override
  void updateHTMLAfterChildrenChange(List<DaxeNode> changed) {
    Timer.run(() => updateEquationDisplay());
  }
}


class TeXMathJaxeDialog {
  TeXMathJax dn;
  ActionFunction _okfct;
  TeXMathJaxeDialog(this.dn, [this._okfct]) {
  }
  
  void show() {
    h.DivElement div1 = new h.DivElement();
    div1.id = 'dlg1';
    div1.classes.add('dlg1');
    h.DivElement div2 = new h.DivElement();
    div2.classes.add('dlg2');
    h.DivElement div3 = new h.DivElement();
    div3.classes.add('dlg3');
    h.FormElement form = new h.FormElement();
    
    h.TextAreaElement ta = new h.TextAreaElement();
    ta.id = 'eqtext';
    if (dn.firstChild != null) {
      ta.value = dn.firstChild.nodeValue.trim();
      adaptTAToText(ta);
    } else {
      ta.value = '\$\$';
      ta.rows = 5;
      ta.cols = 50;
    }
    ta.attributes['spellcheck'] = 'false';
    ta.onInput.listen((h.Event event) => input());
    form.append(ta);
    
    var p = h.document.createElement('p');
    
    var cb = new h.CheckboxInputElement();
    cb.id = 'eqeval';
    cb.checked = dn.getAttribute('eval') == 'on';
    cb.onChange.listen((h.Event event) => input());
    p.append(cb);
    var label = new h.LabelElement();
    label.htmlFor = cb.id;
    label.appendText(LCDStrings.get('evaluate_variables'));
    p.append(label);
    form.append(p);
    
    h.DivElement preview = new h.DivElement();
    preview.id = 'preview';
    form.append(preview);
    
    h.DivElement div_buttons = new h.DivElement();
    div_buttons.classes.add('buttons');
    h.ButtonElement bCancel = new h.ButtonElement();
    bCancel.attributes['type'] = 'button';
    bCancel.appendText(Strings.get("button.Cancel"));
    bCancel.onClick.listen((h.MouseEvent event) {
      div1.remove();
      dn._fixCursorPosition();
    });
    div_buttons.append(bCancel);
    h.ButtonElement bOk = new h.ButtonElement();
    bOk.attributes['type'] = 'submit';
    bOk.appendText(Strings.get("button.OK"));
    bOk.onClick.listen((h.MouseEvent event) => ok(event));
    div_buttons.append(bOk);
    form.append(div_buttons);
    
    div3.append(form);
    div2.append(div3);
    div1.append(div2);
    h.document.body.append(div1);
    
    ta.focus();
    if (ta.value == '\$\$')
      ta.setSelectionRange(1, 1);
    input();
  }
  
  void ok(h.MouseEvent event) {
    h.TextAreaElement ta = h.querySelector('textarea#eqtext');
    String equationText = ta.value;
    h.CheckboxInputElement cb = h.document.getElementById('eqeval');
    if (equationText != '') {
      UndoableEdit compound = new UndoableEdit.compound(Strings.get('undo.insert_text'));
      if (dn.firstChild != null)
        compound.addSubEdit(new UndoableEdit.removeNode(dn.firstChild));
      compound.addSubEdit(new UndoableEdit.insertNode(new Position(dn, 0),
        new DNText(equationText)));
      bool initial_eval = dn.getAttribute('eval') == 'on';
      if (cb.checked != initial_eval) {
        DaxeAttr attr = new DaxeAttr('eval', cb.checked ? 'on' : null);
        compound.addSubEdit(new UndoableEdit.changeAttribute(dn, attr));
      }
      doc.doNewEdit(compound);
    } else {
      if (dn.firstChild != null)
        doc.removeNode(dn.firstChild);
    }
    h.querySelector('div#dlg1').remove();
    if (event != null)
      event.preventDefault();
    if (_okfct != null)
      _okfct();
  }
  
  void input() {
    h.TextAreaElement ta = h.querySelector('textarea#eqtext');
    String text = ta.value;
    adaptTAToText(ta);
    /* newlines allowed -> must use button to insert equations
    if (text.length > 0 && text.contains('\n')) {
      ta.value = text.replaceAll('\n', '');
      ok(null);
      return;
    }
    */
    h.DivElement previewDiv = h.document.getElementById('preview');
    /* this does not work bec. the initial text has not been processed before
    js.JsObject queue = js.context['MathJax']['Hub']['queue'];
    //math = MathJax.Hub.getAllJax("MathOutput")[0]
    js.JsArray all = js.context['MathJax']['Hub'].callMethod('getAllJax', ['preview']);
    js.JsObject math = all[0];
    // MathJax.Hub.queue.Push(['Text', math, "\\displaystyle{$text}"]);
    js.JsObject params = new js.JsObject.jsify(['Text', math, "\\displaystyle{$text}"]);
    queue.callMethod('Push', [params]);
    */
    h.CheckboxInputElement cb = h.document.getElementById('eqeval');
    text = dn.convertForMathJax(text, cb.checked);
    previewDiv.text = text;
    js.JsArray params = new js.JsObject.jsify( ['Typeset', js.context['MathJax']['Hub'], 'preview'] );
    js.context['MathJax']['Hub'].callMethod('Queue', [params]);
  }
  
  /*
   * Adapt textarea dimensions to the equation text, with some limits.
   */
  void adaptTAToText(h.TextAreaElement ta) {
    List<String> lines = ta.value.split('\n');
    int rows = lines.length;
    if (rows < 5)
      rows = 5;
    if (rows > 10)
      rows = 10;
    ta.rows = rows;
    int cols = 50;
    for (String line in lines) {
      if (line.length > cols)
        cols = line.length;
    }
    if (cols > 90)
      cols = 90;
    ta.cols = cols;
  }
}

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>