File:  [LON-CAPA] / modules / damieng / graphical_editor / loncapa_daxe / web / nodes / script_block.dart
Revision 1.11: download - view: text, annotated - select for diffs
Wed Mar 1 20:03:06 2017 UTC (7 years, 3 months ago) by damieng
Branches: MAIN
CVS tags: HEAD
update following a change in Daxe Config API

/*
  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;

/**
 * Script block display for LON-CAPA
 * Jaxe display type: 'script'.
 */
class ScriptBlock extends LCDBlock {
  
  static List<String> perlKeywords = [
    'if', 'unless', 'else', 'elsif', 'while', 'until', 'for', 'each', 'foreach', 'next',
    'last', 'break', 'continue', 'return', 'my', 'our', 'local', 'state', 'BEGIN', 'END',
    'package', 'sub', 'do', 'given ', 'when ', 'default', '__END__', '__DATA__',
    '__FILE__', '__LINE__', '__PACKAGE__'
  ];
  
  static List<String> javascriptKeywords = [
    'abstract', 'arguments', 'boolean', 'break', 'byte',
    'case', 'catch', 'char', 'class*', 'const',
    'continue', 'debugger', 'default', 'delete', 'do',
    'double', 'else', 'enum*', 'eval', 'export*',
    'extends*', 'false', 'final', 'finally', 'float',
    'for', 'function', 'goto', 'if', 'implements',
    'import*', 'in', 'instanceof', 'int', 'interface',
    'let', 'long', 'native', 'new', 'null',
    'package', 'private', 'protected', 'public', 'return',
    'short', 'static', 'super*', 'switch', 'synchronized',
    'this', 'throw', 'throws', 'transient', 'true',
    'try', 'typeof', 'var', 'void', 'volatile',
    'while', 'with', 'yield'
  ];
  
  String currentType = null;
  
  
  ScriptBlock.fromRef(x.Element elementRef) : super.fromRef(elementRef) {
  }
  
  ScriptBlock.fromNode(x.Node node, DaxeNode parent) : super.fromNode(node, parent) {
    if (firstChild is DNText && firstChild.nextSibling == null) {
      String text = firstChild.nodeValue;
      // remove one newline after <![CDATA[ and before ]]>
      if (text.startsWith('\n'))
        text = text.substring(1);
      if (text.endsWith('\n'))
        text = text.substring(0, text.length - 1);
      firstChild.replaceWith(new ScriptText(text));
    }
  }
  
  @override
  h.Element html() {
    h.Element div = super.html();
    if (firstChild == null || firstChild.nextSibling != null || firstChild is! ScriptText)
      return div;
    if (state != 2 && hasContent) {
      h.DivElement contents = div.lastChild;
      String type = _getTypeValue();
      currentType = type;
      if (type == 'loncapa/perl' || type == 'text/javascript') {
        contents.classes.add('script-text');
        contents.style.position = 'relative';
        h.DivElement overlay;
        if (type == 'loncapa/perl')
          overlay = createPerlOverlay();
        else
          overlay = createJavascriptOverlay();
        contents.append(overlay);
      } else {
        contents.style.fontFamily = 'monospace';
      }
    }
    return div;
  }
  
  String _getTypeValue() {
    String type = getAttribute('type');
    if (type == null) {
      // check if there is a default value
      x.Element attRef = doc.cfg.attributeReference(ref, 'type', null);
      type = doc.cfg.defaultAttributeValue(attRef);
    }
    return type;
  }
  
  void addLineNumber(div, sb, line_number) {
    if (sb.length > 0) {
      div.append(new h.Text(sb.toString()));
      sb.clear();
    }
    h.SpanElement span1 = new h.SpanElement();
    span1.classes.add('script-line-number-position');
    h.SpanElement span2 = new h.SpanElement();
    span2.classes.add('script-line_number');
    String s = line_number.toString();
    span2.appendText(s);
    span1.append(span2);
    div.append(span1);
  }
  
  h.Element createPerlOverlay() {
    // NOTE: this is very basic, we might need a real parser to do something more complex
    // TODO: add special highlighting for string redirect << and regular expressions 
    final String letters = '\$@%&abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'; // starting chars in names
    final String digits = '0123456789';
    h.DivElement div = new h.DivElement();
    div.classes.add('script-colored');
    String text = firstChild.nodeValue;
    StringBuffer sb = new StringBuffer();
    bool in_name = false;
    bool in_comment = false;
    bool in_number = false;
    bool in_string = false;
    bool in_backslash = false;
    String string_start;
    int i = 0;
    int line_number = 1;
    if (text.length > 0) {
      addLineNumber(div, sb, line_number);
      line_number++;
    }
    while (i < text.length) {
      String c = text[i];
      if (!in_string && !in_comment && (
          (letters.contains(c) && !((c == 'e' || c == 'E') && in_number)) ||
          (in_name && digits.contains(c)) ||
          (in_name && c == '#' && sb.toString()[sb.length-1] == '\$'))) {
        if (!in_name ) {
          if (sb.length > 0) {
            div.append(new h.Text(sb.toString()));
            sb.clear();
          }
          in_name = true;
        }
      } else if (!in_string && !in_name && !in_comment && (digits.contains(c) ||
          ((c == '.' || c == 'e' || c == 'E') && in_number))) {
        if (!in_number) {
          if (sb.length > 0) {
            div.append(new h.Text(sb.toString()));
            sb.clear();
          }
          in_number = true;
        }
      } else {
        if (in_name) {
          String s = sb.toString();
          h.SpanElement span = new h.SpanElement();
          if (perlKeywords.contains(s))
            span.classes.add('keyword');
          else if (s.startsWith('\$') || s.startsWith('@') || s.startsWith('%'))
            span.classes.add('variable');
          else if (s.startsWith('&') || c == '(')
            span.classes.add('function-call');
          else
            span.classes.add('name');
          span.appendText(s);
          div.append(span);
          sb.clear();
          in_name = false;
        } else if (in_number) {
          h.SpanElement span = new h.SpanElement();
          span.classes.add('number');
          span.appendText(sb.toString());
          div.append(span);
          sb.clear();
          in_number = false;
        } else if (in_comment && (c == '\n' || c == '\r')) {
          h.SpanElement span = new h.SpanElement();
          span.classes.add('comment');
          span.appendText(sb.toString());
          div.append(span);
          sb.clear();
          in_comment = false;
        }
        if (!in_comment && (c == '"' || c == "'") && !in_backslash) {
          if (in_string) {
            if (c == string_start) {
              h.SpanElement span = new h.SpanElement();
              span.classes.add('string');
              span.appendText(sb.toString());
              div.append(span);
              sb.clear();
              in_string = false;
            }
          } else {
            if (sb.length > 0) {
              div.append(new h.Text(sb.toString()));
              sb.clear();
            }
            string_start = c;
            in_string = true;
          }
        } else if (!in_string && c == '#' && !in_comment) {
          if (sb.length > 0) {
            div.append(new h.Text(sb.toString()));
            sb.clear();
          }
          in_comment = true;
        }
      }
      if (in_string) {
        if (c == '\\')
          in_backslash = !in_backslash;
        else if (in_backslash)
          in_backslash = false;
        if (c == '\n') {
          // multiline string, need to split it because of line numbers
          h.SpanElement span = new h.SpanElement();
          span.classes.add('string');
          span.appendText(sb.toString());
          div.append(span);
          sb.clear();
        }
      }
      sb.write(c);
      if (c == '\n') {
        addLineNumber(div, sb, line_number);
        line_number++;
      }
      i++;
    }
    if (sb.length > 0) {
      if (in_comment) {
        h.SpanElement span = new h.SpanElement();
        span.classes.add('comment');
        span.appendText(sb.toString());
        div.append(span);
      } else
        div.append(new h.Text(sb.toString()));
    }
    return(div);
  }
  
  h.Element createJavascriptOverlay() {
    // NOTE: this is very basic, we might need a real parser to do something more complex
    final String letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'; // starting chars in names
    final String digits = '0123456789';
    h.DivElement div = new h.DivElement();
    div.classes.add('script-colored');
    String text = firstChild.nodeValue;
    StringBuffer sb = new StringBuffer();
    bool in_name = false;
    bool in_comment = false;
    bool in_oneline_comment = false;
    bool in_multiline_comment = false;
    bool in_number = false;
    bool in_string = false;
    bool in_backslash = false;
    String c2; // last character in sb, to avoid calling sb.toString() in the tests
    String string_start;
    int i = 0;
    int line_number = 1;
    if (text.length > 0) {
      addLineNumber(div, sb, line_number);
      line_number++;
    }
    while (i < text.length) {
      String c = text[i];
      if (!in_string && !in_comment && (
          (letters.contains(c) && !((c == 'e' || c == 'E') && in_number)) ||
          (in_name && digits.contains(c)))) {
        if (!in_name ) {
          if (sb.length > 0) {
            div.append(new h.Text(sb.toString()));
            sb.clear();
            c2 = '';
          }
          in_name = true;
        }
      } else if (!in_string && !in_name && !in_comment &&
          (digits.contains(c) || ((c == '.' || c == 'e' || c == 'E') && in_number))) {
        if (!in_number) {
          if (sb.length > 0) {
            div.append(new h.Text(sb.toString()));
            sb.clear();
            c2 = '';
          }
          in_number = true;
        }
      } else {
        if (in_name) {
          String s = sb.toString();
          h.SpanElement span = new h.SpanElement();
          if (javascriptKeywords.contains(s))
            span.classes.add('keyword');
          else if (c == '(')
            span.classes.add('function-call');
          else
            span.classes.add('name');
          span.appendText(s);
          div.append(span);
          sb.clear();
          c2 = '';
          in_name = false;
        } else if (in_number) {
          h.SpanElement span = new h.SpanElement();
          span.classes.add('number');
          span.appendText(sb.toString());
          div.append(span);
          sb.clear();
          c2 = '';
          in_number = false;
        } else if (in_oneline_comment && (c == '\n' || c == '\r')) {
          h.SpanElement span = new h.SpanElement();
          span.classes.add('comment');
          span.appendText(sb.toString());
          div.append(span);
          sb.clear();
          c2 = '';
          in_comment = false;
          in_oneline_comment = false;
        }
        if (!in_comment && (c == '"' || c == "'") && !in_backslash) {
          // strings
          if (in_string) {
            if (c == string_start) {
              h.SpanElement span = new h.SpanElement();
              span.classes.add('string');
              span.appendText(sb.toString());
              div.append(span);
              sb.clear();
              c2 = '';
              in_string = false;
            }
          } else {
            if (sb.length > 0) {
              div.append(new h.Text(sb.toString()));
              sb.clear();
              c2 = '';
            }
            string_start = c;
            in_string = true;
          }
        } else if (!in_string && (c == '/' || c == '*')) {
          // comments
          if (!in_comment) {
            if (c2 == '/') {
              String s = sb.toString();
              div.append(new h.Text(s.substring(0, s.length - 1)));
              sb.clear();
              sb.write(c2);
              in_comment = true;
              if (c == '/')
                in_oneline_comment = true;
              else if (c == '*')
                in_multiline_comment = true;
            }
          } else if (in_multiline_comment) {
            if (c2 == '*' && c == '/') {
              h.SpanElement span = new h.SpanElement();
              span.classes.add('comment');
              sb.write(c);
              span.appendText(sb.toString());
              div.append(span);
              sb.clear();
              c2 = '';
              in_comment = false;
              in_multiline_comment = false;
              i++;
              continue;
            }
          }
        } else if (in_multiline_comment && c == '\n') {
          // multiline comment, need to split it because of line numbers
          h.SpanElement span = new h.SpanElement();
          span.classes.add('comment');
          span.appendText(sb.toString());
          div.append(span);
          sb.clear();
        }
      }
      if (in_string) {
        if (c == '\\')
          in_backslash = !in_backslash;
        else if (in_backslash)
          in_backslash = false;
      }
      sb.write(c);
      if (c == '\n') {
        addLineNumber(div, sb, line_number);
        line_number++;
      }
      c2 = c;
      i++;
    }
    if (sb.length > 0) {
      if (in_comment) {
        h.SpanElement span = new h.SpanElement();
        span.classes.add('comment');
        span.appendText(sb.toString());
        div.append(span);
      } else
        div.append(new h.Text(sb.toString()));
    }
    return(div);
  }
  
  @override
  void updateHTMLAfterChildrenChange(List<DaxeNode> changed) {
    updateHTML();
  }
  
  bool get needsSpecialDNText {
    return true;
  }
  
  @override
  DNText specialDNTextConstructor(String text) {
    return new ScriptText(text);
  }
  
  /**
   * Speed optimization: instead of calling updateHTML() for
   * each inserted character, only the block contents are updated.
   */
  void textUpdate() {
    h.DivElement div = getHTMLNode();
    if (state != 2 && hasContent && div != null) {
      h.DivElement contents = div.lastChild;
      String type = _getTypeValue();
      if (type == currentType) {
        if (type == 'loncapa/perl' || type == 'text/javascript') {
          h.DivElement overlay;
          if (type == 'loncapa/perl')
            overlay = createPerlOverlay();
          else
            overlay = createJavascriptOverlay();
          h.SpanElement rawText = contents.firstChild;
          rawText.text = firstChild.nodeValue;
          // FIXME: this is slow for long scripts, we should split the lines
          // in html() and change only the relevant lines here
          // (but scripts are supposed to be short in LC problems).
          h.DivElement oldOverlay = contents.lastChild;
          oldOverlay.replaceWith(overlay);
          return;
        }
      }
    }
    super.updateHTML();
  }
  
  @override
  void updateAttributes() {
    // this is useful for undo/redo
    if (state == 2)
      return;
    String type = _getTypeValue();
    if (currentType != type) {
      updateHTML();
    } else {
      super.updateAttributes();
    }
  }
  
  @override
  void changeAttributeValue(x.Element refAttr, SimpleTypeControl attributeControl) {
    // LCDBlock uses updateDisplay:false, so we need to override this
    // to update the display when the type attribute is changed.
    // Full element updates are limited to relevant attribute changes.
    super.changeAttributeValue(refAttr, attributeControl);
    if (doc.cfg.attributeName(refAttr) == 'type') {
      if (state == 2)
        return;
      String type = _getTypeValue();
      if (currentType != type) {
        if (type == 'loncapa/perl' || type == 'text/javascript' ||
            currentType == 'loncapa/perl' || currentType == 'text/javascript') {
          updateHTML();
          attributeControls['type'].focus();
        }
      }
    }
  }
  
  /**
   * Override to use a CDATA section.
   */
  @override
  x.Node toDOMNode(x.Document domDocument) {
    if (firstChild == null || firstChild is! DNText || firstChild.nextSibling != null)
      return super.toDOMNode(domDocument);
    String text = '\n' + firstChild.nodeValue + '\n';
    x.Element el = domDocument.createElementNS(namespaceURI, nodeName);
    for (DaxeAttr att in attributes)
      el.setAttributeNS(att.namespaceURI, att.name, att.value);
    x.CDATASection cdata = domDocument.createCDATASection(text);
    el.appendChild(cdata);
    return(el);
  }
}

/**
 * DNText that calls parent.textUpdate() instead of updateHTML().
 */
class ScriptText extends DNText {
  
  ScriptText(String s) : super(s);
  
  ScriptText.fromNode(x.Node node, DaxeNode parent) : super.fromNode(node, parent);
  
  @override
  void updateHTML() {
    (parent as ScriptBlock).textUpdate();
  }
  
}

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