<commit_before>/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
 * Copyright 2013 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*global AS2TextField */

var TextFieldDefinition = (function () {

  var xmlParser = new XMLParser();

  /*
   * Parsing "html", in this context, actually means ensuring that the given
   * string is valid *xml*, and removing any tags that mustn't be supported.
   *
   * After that, two things are generated: a plain-text version of the content,
   * and a list of ranges with types and attributes, representing all nested
   * nodes.
   */
  function parseHtml(val) {
    var xml = xmlParser.parseFromString(val).children[0];
    var content = {ranges : [], text : ''};
    convertXML(xml, content);
    return content;
  }

  function convertXML(xml, content) {
    // Ignore all comments, processing instructions and namespaced nodes.
    if (xml.kind !== 'element' && xml.kind !== 'text' ||
        xml.name.prefix !== '')
    {
      return;
    }
    var range = {start : content.text.length};
    content.ranges.push(range);

    if (xml.kind === 'text') {
      var text = xml.value;
      range.type = 'text';
      range.text = text;
      content.text += text;
      range.end = content.text.length;
      return;
    }

    range.type = xml.name.localName;
    range.attributes = extractAttributes(xml);

    var children = xml.children;
    var childCount = children.length;
    for (var i = 0; i < childCount; i++) {
      convertXML(children[i], content);
    }
    range.end = content.text.length;
  }

  /**
   * Creates an object containing all attributes with their localName as keys.
   * Ignores all namespaced attributes, as we don't need them for the
   * TextField's purposes.
   * TODO: check whether we still have to keep them around for round-tripping.
   */
  function extractAttributes(node) {
    var attributesList = node.attributes;
    var attributesMap = Object.create(null);
    for (var i = 0; i < attributesList.length; i++) {
      var attr = attributesList[i];
      if (attr.name.prefix !== '') {
        continue;
      }
      attributesMap[attr.name.localName] = attr.value;
    }
    return attributesMap;
  }

  function renderRanges(ranges) {
    var output = '';
    var length = 0;
    var rangeStack = [];
    var currentRange = null;

    for (var i = 0; i < ranges.length; i++) {
      var range = ranges[i];
      while (currentRange && currentRange.end <= range.start) {
        output += renderRangeEnd(currentRange);
        rangeStack.pop();
        currentRange = rangeStack[rangeStack.length - 1];
      }
      assert(range.start === length);
      if (range.type === 'text') {
        output += range.text;
        length = range.end;
        continue;
      }
      output += renderRangeStart(range);
      currentRange = range;
      rangeStack.push(range);
    }
    while (rangeStack.length) {
      output += renderRangeEnd(rangeStack.pop());
    }
    console.log(output);
    return output;
  }
  function renderRangeStart(range) {
    var attributes = range.attributes;
    var output;
    var styles;
    switch (range.type) {
      case 'br': return '<br />';
      case 'b': return '<strong>';
      case 'i': return '<i>';
      // TODO: check if we need to emit <ul>'s around runs of <li>'s
      case 'li': return '<li>';
      case 'u': return '<span style="text-decoration: underline;">';
      case 'a': {
        output = '<a';
        if ('href' in attributes) {
          output += ' href="' + attributes.href + '"';
        }
        if ('target' in attributes) {
          output += ' target="' + attributes.href + '"';
        }
        break;
      }
      case 'font': {
        output = '<span';
        styles = '';
        if ('color' in attributes) {
          styles += 'color:' + attributes.color + ';';
        }
        if ('face' in attributes) {
          var fontFace = attributes.face;
          if (fontFace === '_sans') {
            fontFace = 'sans-serif';
          } else if (fontFace === '_serif') {
            fontFace = 'serif';
          }
          //TODO: adapt to embedded font names
          styles += 'font-family:' + fontFace + ';';
        }
        if ('size' in attributes) {
          //TODO: verify that px is the right unit
          styles += 'font-size:' + attributes.size + 'px;';
        }
        break;
      }
      case 'img': {
        output = '<img src="' + (attributes.src || '') + '"';
        styles = '';
        if ('width' in attributes) {
          styles += 'width:' + attributes.width + 'px;';
        }
        if ('height' in attributes) {
          styles += 'height:' + attributes.height + 'px;';
        }
        if ('hspace' in attributes && 'vspace' in attributes) {
          styles += 'margin:' + attributes.vspace + 'px ' +
                    attributes.hspace + 'px;';
        } else if ('hspace' in attributes) {
          styles += 'margin:0 ' + attributes.hspace + 'px;';
        } else if ('vspace' in attributes) {
          styles += 'margin:' + attributes.hspace + 'px 0;';
        }
        // TODO: support `align`, `id` and `checkPolicyFile`
        output += ' /';
        break;
      }
      case 'p': {
        output = '<p';
        styles = '';
        if ('class' in attributes) {
          styles += ' class="' + attributes['class'] + '"';
        }
        if ('align' in attributes) {
          styles += 'text-align:' + attributes.align;
        }
        break;
      }
      case 'span': {
        output = '<span';
        if ('class' in attributes) {
          output += ' class="' + attributes['class'] + '"';
        }
        break;
      }
      case 'textformat': {
        // TODO: we probably need to merge textformat nodes with <p> nodes
        output = '<span';
        var styles = '';
        var marginLeft = 0;
        if ('blockIdent' in attributes) {
          marginLeft += parseInt(attributes.blockIndent);
        }
        if ('leftMargin' in attributes) {
          marginLeft += parseInt(attributes.leftMargin);
        }
        if (marginLeft !== 0) {
          styles += 'margin-left:' + marginLeft + 'px"';
        }
        if ('indent' in attributes) {
          styles += 'text-indent:' + attributes.indent + 'px"';
        }
        if ('rightMargin' in attributes) {
          styles += 'margin-left:' + marginLeft + 'px"';
        }
        // TODO: support leading
        // TODO: support tabStops, if possible
        break;
      }
      default:
        // For all unknown nodes, we just emit spans. We might not actually
        // need to, but it doesn't do any harm, either.
        return '<span>';
    }
    if (styles) {
      output += ' style="' + styles + '"';
    }
    return output + '>';
  }
  function renderRangeEnd(range) {
    switch (range.type) {
      case 'b': return '</strong>';
      case 'i':
      case 'li':
      case 'a':
      case 'p': {
        return '</' + range.type + '>';
      }
      default: { // <u>, <font>, <span>, <textformat>, and all others
        return '</span>';
      }
    }
  }

  var def = {
    __class__: 'flash.text.TextField',

    initialize: function () {
      this._defaultTextFormat = null;
      this._type = 'dynamic';
      this._textHeight = 0;
      this._textWidth = 0;
      this._embedFonts = false;

      var bbox = this._bbox;
      if (bbox) {
        // // TODO: use canvas.measureText(txt).height
        this._textHeight = bbox.bottom - bbox.top;
      }

      var s = this.symbol;
      if (!s) {
        return;
      }

      var tag = s.tag;
      var bbox = tag.bbox;
      this._elementWidth = (bbox.right - bbox.left);
      this._elementHeight = (bbox.bottom - bbox.top);
      //TODO: we might need to move things down and right by 2px, or add a 2px margin
      this.boundsStyle = 'top:' + bbox.top + 'px;left:' + bbox.left + 'px;' +
                         'width:' + this._elementWidth + 'px;' +
                         'height:' + this._elementHeight + 'px;';

      if (tag.initialText) {
        if (tag.html) {
          this.htmlText = tag.initialText;
        } else {
          this.text = tag.initialText;
        }
        this._contentChanged = true;
      }
    },

    _getAS2Object: function () {
      if (!this.$as2Object) {
        new AS2TextField().$attachNativeObject(this);
      }
      return this.$as2Object;
    },

    replaceText: function(begin, end, str) {
      this._text = this._text.substring(0, begin) + str + this._text.substring(end);
      this._markAsDirty();
    },

    draw: function (ctx) {
      var bbox = this._bbox;
      var content = this._contentChanged === false ? this._renderedContent :
          "<svg xmlns='http://www.w3.org/2000/svg' " +
          "width='" + this._elementWidth +
          "' height='" + this._elementHeight + "'>" +
          "<foreignObject width='100%' height='100%'>" +
          "<div xmlns='http://www.w3.org/1999/xhtml' style='" +
          this.boundsStyle + "'>" + renderRanges(this._content.ranges) +
          '</div></foreignObject></svg>';

      this._renderedContent = content;
      this._contentChanged = false;

      var img = new Image();
      var svg = new Blob([content], {type: "image/svg+xml;charset=utf-8"});
      var DOMURL = self.URL || self.webkitURL || self;
      var url = DOMURL.createObjectURL(svg);
      img.onload = function() {
        ctx.drawImage(img, 0, 0);
        DOMURL.revokeObjectURL(url);
      };
      img.src = url;
    },

    get text() {
      return this._text;
    },
    set text(val) {
      if (this._text === val) {
        return;
      }
      //TODO: add default markup.
      var range = {type : 'text', text: val, start : 0, end : val.length};
      this._content = {ranges : [range], text : val};<commit_msg>Change TextField html parsing and rendering to work on a nested tree instead of a linear list of ranges<commit_after>/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
 * Copyright 2013 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*global AS2TextField */

var TextFieldDefinition = (function () {

  var xmlParser = new XMLParser();

  /*
   * Parsing "html", in this context, actually means ensuring that the given
   * string is valid *xml*, and removing any tags that mustn't be supported.
   *
   * After that, two things are generated: a plain-text version of the content,
   * and a list of ranges with types and attributes, representing all nested
   * nodes.
   */
  function parseHtml(val) {
    var xml = xmlParser.parseFromString(val).children[0];
    var content = {text : '', tree : null};
    content.tree = convertXML(xml, content);
    return content;
  }

  function convertXML(xml, content) {
    // Ignore all comments, processing instructions and namespaced nodes.
    if (xml.kind !== 'element' && xml.kind !== 'text' ||
        xml.name.prefix !== '')
    {
      return;
    }
    var node = {type : '', text : '', format : null, children : null};

    if (xml.kind === 'text') {
      var text = xml.value;
      node.type = 'text';
      node.text = text;
      content.text += text;
      return node;
    }

    node.type = xml.name.localName;
    node.format = extractAttributes(xml);
    node.children = [];

    var children = xml.children;
    var childCount = children.length;
    for (var i = 0; i < childCount; i++) {
      node.children.push(convertXML(children[i], content));
    }

    return node;
  }

  /**
   * Creates an object containing all attributes with their localName as keys.
   * Ignores all namespaced attributes, as we don't need them for the
   * TextField's purposes.
   * TODO: check whether we still have to keep them around for round-tripping.
   */
  function extractAttributes(node) {
    var attributesList = node.attributes;
    var attributesMap = Object.create(null);
    for (var i = 0; i < attributesList.length; i++) {
      var attr = attributesList[i];
      if (attr.name.prefix !== '') {
        continue;
      }
      attributesMap[attr.name.localName] = attr.value;
    }
    return attributesMap;
  }

  function renderContent(content, ctx) {
    return renderNode(content.tree, ctx);
  }

  function renderNode(node, ctx) {
    if (node.type === 'text') {
      return node.text;
    }

    var output = renderNodeStart(node, ctx);
    var children = node.children;

    for (var i = 0; i < children.length; i++) {
      var childNode = children[i];
      output += renderNode(childNode, ctx);
    }
    return output + renderNodeEnd(node, ctx);
  }

  function renderNodeStart(node, ctx) {
    var format = node.format;
    var output;
    var styles;
    switch (node.type) {
      case 'br': return '<br />';
      case 'b': return '<strong>';
      case 'i': return '<i>';
      // TODO: check if we need to emit <ul>'s around runs of <li>'s
      case 'li': return '<li>';
      case 'u': return '<span style="text-decoration: underline;">';
      case 'a': {
        output = '<a';
        if ('href' in format) {
          output += ' href="' + format.href + '"';
        }
        if ('target' in format) {
          output += ' target="' + format.href + '"';
        }
        break;
      }
      case 'font': {
        output = '<span';
        styles = '';
        if ('color' in format) {
          styles += 'color:' + format.color + ';';
        }
        if ('face' in format) {
          var fontFace = format.face;
          if (fontFace === '_sans') {
            fontFace = 'sans-serif';
          } else if (fontFace === '_serif') {
            fontFace = 'serif';
          }
          //TODO: adapt to embedded font names
          styles += 'font-family:' + fontFace + ';';
        }
        if ('size' in format) {
          //TODO: verify that px is the right unit
          styles += 'font-size:' + format.size + 'px;';
        }
        break;
      }
      case 'img': {
        output = '<img src="' + (format.src || '') + '"';
        styles = '';
        if ('width' in format) {
          styles += 'width:' + format.width + 'px;';
        }
        if ('height' in format) {
          styles += 'height:' + format.height + 'px;';
        }
        if ('hspace' in format && 'vspace' in format) {
          styles += 'margin:' + format.vspace + 'px ' +
                    format.hspace + 'px;';
        } else if ('hspace' in format) {
          styles += 'margin:0 ' + format.hspace + 'px;';
        } else if ('vspace' in format) {
          styles += 'margin:' + format.hspace + 'px 0;';
        }
        // TODO: support `align`, `id` and `checkPolicyFile`
        output += ' /';
        break;
      }
      case 'p': {
        output = '<p';
        styles = '';
        if ('class' in format) {
          styles += ' class="' + format['class'] + '"';
        }
        if ('align' in format) {
          styles += 'text-align:' + format.align;
        }
        break;
      }
      case 'span': {
        output = '<span';
        if ('class' in format) {
          output += ' class="' + format['class'] + '"';
        }
        break;
      }
      case 'textformat': {
        // TODO: we probably need to merge textformat nodes with <p> nodes
        output = '<span';
        styles = '';
        var marginLeft = 0;
        if ('blockIdent' in format) {
          marginLeft += parseInt(format.blockIndent);
        }
        if ('leftMargin' in format) {
          marginLeft += parseInt(format.leftMargin);
        }
        if (marginLeft !== 0) {
          styles += 'margin-left:' + marginLeft + 'px"';
        }
        if ('indent' in format) {
          styles += 'text-indent:' + format.indent + 'px"';
        }
        if ('rightMargin' in format) {
          styles += 'margin-left:' + marginLeft + 'px"';
        }
        // TODO: support leading
        // TODO: support tabStops, if possible
        break;
      }
      default:
        // For all unknown nodes, we just emit spans. We might not actually
        // need to, but it doesn't do any harm, either.
        return '<span>';
    }
    if (styles) {
      output += ' style="' + styles + '"';
    }
    return output + '>';
  }

  function renderNodeEnd(node, ctx) {
    switch (node.type) {
      case 'b': return '</strong>';
      case 'i':
      case 'li':
      case 'a':
      case 'p': {
        return '</' + node.type + '>';
      }
      default: { // <u>, <font>, <span>, <textformat>, and all others
        return '</span>';
      }
    }
  }

  var def = {
    __class__: 'flash.text.TextField',

    initialize: function () {
      this._defaultTextFormat = null;
      this._type = 'dynamic';
      this._textHeight = 0;
      this._textWidth = 0;
      this._embedFonts = false;

      var bbox = this._bbox;
      if (bbox) {
        // // TODO: use canvas.measureText(txt).height
        this._textHeight = bbox.bottom - bbox.top;
      }

      var s = this.symbol;
      if (!s) {
        return;
      }

      var tag = s.tag;
      var bbox = this._bbox = tag.bbox;
      this._elementWidth = (bbox.right - bbox.left);
      this._elementHeight = (bbox.bottom - bbox.top);
      //TODO: we might need to move things down and right by 2px, or add a 2px margin
      this.boundsStyle = 'top:' + bbox.top + 'px;left:' + bbox.left + 'px;' +
                         'width:' + this._elementWidth + 'px;' +
                         'height:' + this._elementHeight + 'px;';

      if (tag.initialText) {
        if (tag.html) {
          this.htmlText = tag.initialText;
        } else {
          this.text = tag.initialText;
        }
        this._contentChanged = true;
      }
    },

    _getAS2Object: function () {
      if (!this.$as2Object) {
        new AS2TextField().$attachNativeObject(this);
      }
      return this.$as2Object;
    },

    replaceText: function(begin, end, str) {
      this._text = this._text.substring(0, begin) + str + this._text.substring(end);
      this._markAsDirty();
    },

    draw: function (ctx) {
      var content = this._contentChanged === false ? this._renderedContent :
          "<svg xmlns='http://www.w3.org/2000/svg' " +
          "width='" + this._elementWidth +
          "' height='" + this._elementHeight + "'>" +
          "<foreignObject width='100%' height='100%'>" +
          "<div xmlns='http://www.w3.org/1999/xhtml' style='" +
          this.boundsStyle + "'>" + renderContent(this._content) +
          '</div></foreignObject></svg>';

      this._renderedContent = content;
      this._contentChanged = false;

      var img = new Image();
      var svg = new Blob([content], {type: "image/svg+xml;charset=utf-8"});
      var DOMURL = self.URL || self.webkitURL || self;
      var url = DOMURL.createObjectURL(svg);
      img.onload = function() {
        ctx.drawImage(img, 0, 0);
        DOMURL.revokeObjectURL(url);
      };
      img.src = url;
    },

    get text() {
      return this._text;
    },
    set text(val) {
      if (this._text === val) {
        return;
      }
      //TODO: add default markup.
      var node = {type : 'text', text: val, format : {}, children : null};
      this._content = {tree : [node], text : val};
{'fraction_non_alphanumeric': 0.14558917275535724, 'fraction_numerical': 0.004952679841121954, 'mean_word_length': 2.004862236628849, 'pattern_counts': {'":': 0, '<': 66, '<?xml version=': 0, '>': 49, 'https://': 0, 'lorem ipsum': 0, 'www.': 6, 'xml': 34}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7121128', 'n_tokens_mistral': 6132, 'n_tokens_neox': 5980, 'n_words': 1740}