'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});
exports['default'] = parseHtml;

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

var _attributes = require('./attributes');

var _dom = require('./dom');

var _lodash = require('lodash');

var _lodash2 = _interopRequireDefault(_lodash);

// U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), U+000A LINE FEED (LF), U+000C FORM FEED (FF), and U+000D CARRIAGE RETURN (CR)
// (https://html.spec.whatwg.org/multipage/infrastructure.html#space-character)
var SPACE_CHARS = ' \t\n\f\r';
var SPACE_CHARS_EXCEPT_LF = ' \t\f\r';

function insertIntoContainer(node, container) {
  var children = document.createDocumentFragment();
  while (node.childNodes.length > 0) {
    children.appendChild(node.childNodes[0]);
  }
  container.appendChild(children);
}

function verticalAlignForElement(node, initialStyle) {
  var verticalAlign = undefined;
  // why isn't there a simple way to do this??
  while (node && node.nodeType === Node.ELEMENT_NODE) {
    // tagged superscript/subscript computed style values are correct in Chrome/Firefox but not IE, use the tag first
    if (node.nodeName === 'SUP') {
      verticalAlign = 'super';
    } else if (node.nodeName === 'SUB') {
      verticalAlign = 'sub';
    } else {
      var style = initialStyle ? initialStyle : getComputedStyle(node, null);
      var nodeAlign = style.getPropertyValue('vertical-align');
      if (nodeAlign && nodeAlign.length > 0 && nodeAlign !== 'baseline') {
        verticalAlign = nodeAlign;
      }
    }
    initialStyle = null;
    node = node.parentNode;
  }
  return verticalAlign;
}

function textDecorationsForElement(node, initialStyle) {
  var decorations = undefined;
  // this is going to be removed by webkit/blink but could be useful for now
  // https://code.google.com/p/chromium/issues/detail?id=269140
  if (initialStyle.webkitTextDecorationsInEffect) {
    return initialStyle.webkitTextDecorationsInEffect;
  }
  // this is simplistic but should generally work ok (does not take into account out of flow and inline level elements)
  // why isn't there a simple way to do this??
  while (node && node.nodeType === Node.ELEMENT_NODE) {
    var style = initialStyle ? initialStyle : getComputedStyle(node, null);
    initialStyle = null;
    var nodeDecorations = style.getPropertyValue('text-decoration');
    if (nodeDecorations && nodeDecorations.length > 0 && nodeDecorations !== 'none') {
      if (decorations && decorations.length > 0 && decorations !== 'none') {
        var d = decorations.split(' ');
        d.push(nodeDecorations.split(' '));
        decorations = _lodash2['default'].uniq(d).join(' ');
      } else {
        decorations = nodeDecorations;
      }
    }
    node = node.parentNode;
  }
  return decorations;
}

function attributesForElement(node, style) {
  if (_lodash2['default'].isUndefined(style)) style = getComputedStyle(node, null);
  var attrs = {};
  var fontWeight = (0, _dom.normalizeFontWeight)(style.getPropertyValue('font-weight'));
  if (fontWeight === 'bold') {
    attrs[_attributes.ATTR.BOLD] = true;
  }
  if (style.getPropertyValue('font-style') === 'italic') {
    attrs[_attributes.ATTR.ITALIC] = true;
  }
  // vertical align does not inherit, so we need to check ancestors
  var verticalAlign = verticalAlignForElement(node, style);
  if (verticalAlign && verticalAlign === 'super') {
    attrs[_attributes.ATTR.SUPERSCRIPT] = true;
  } else if (verticalAlign === 'sub') {
    attrs[_attributes.ATTR.SUBSCRIPT] = true;
  }
  // text decoration does not inherit, so we need to check ancestors (http://stackoverflow.com/a/1823388/430128)
  var textDecorations = textDecorationsForElement(node, style);
  if (textDecorations && textDecorations.indexOf('underline') > -1) {
    attrs[_attributes.ATTR.UNDERLINE] = true;
  }
  if (textDecorations && textDecorations.indexOf('line-through') > -1) {
    attrs[_attributes.ATTR.STRIKETHROUGH] = true;
  }
  return attrs;
}

/**
 * https://rawgit.com/timdown/rangy/master/fiddlings/spec/innerText.htm#ignored-node
 * @param node
 * @returns {boolean}
 */
function ignoredNode(node) {
  return node.nodeType === Node.COMMENT_NODE || node.nodeType === Node.PROCESSING_INSTRUCTION_NODE || node.nodeType === Node.ELEMENT_NODE && getComputedStyle(node, null).getPropertyValue('display') === 'none';
}

/**
 * https://rawgit.com/timdown/rangy/master/fiddlings/spec/innerText.htm#leading-whitespace
 * @param el
 */
function elementLeadingWhitespace(_x) {
  var _again = true;

  _function: while (_again) {
    var el = _x;
    style = display = firstNotIgnoredChild = first = undefined;
    _again = false;

    var style = getComputedStyle(el, null);
    var display = style.getPropertyValue('display');

    var firstNotIgnoredChild = function firstNotIgnoredChild() {
      for (var i = 0; i < el.children.length; i++) {
        var n = el.children[i];
        if (!ignoredNode(n)) return n;
      }
      return null;
    };

    if (display === 'inline') {
      var first = firstNotIgnoredChild();
      if (first && first.nodeType === Node.ELEMENT_NODE) {
        _x = first;
        _again = true;
        continue _function;
      } else {
        return '';
      }
    } else if (display === 'inline-block' || display === 'inline-table' || display === 'none' || display === 'table-cell' || display === 'table-column' || display === 'table-column-group') {
      return '';
    } else {
      return '\n';
    }
  }
}

/**
 * https://rawgit.com/timdown/rangy/master/fiddlings/spec/innerText.htm#trailing-whitespace
 * @param el
 */
function elementTrailingWhitespace(_x2) {
  var _again2 = true;

  _function2: while (_again2) {
    var el = _x2;
    style = display = lastNotIgnoredChild = last = lastChunks = elChunks = marginBottom = fontSize = undefined;
    _again2 = false;

    var style = getComputedStyle(el, null);
    var display = style.getPropertyValue('display');

    var lastNotIgnoredChild = function lastNotIgnoredChild() {
      for (var i = el.children.length - 1; i >= 0; i--) {
        var n = el.children[i];
        if (!ignoredNode(n)) return n;
      }
      return null;
    };

    if (display === 'inline') {
      var last = lastNotIgnoredChild();
      if (last && last.nodeType === Node.ELEMENT_NODE) {
        var lastChunks = [];
        innerRichText(last, lastChunks);
        _x2 = last;
        lastChunks;
        _again2 = true;
        continue _function2;
      } else {
        return '';
      }
    } else if (display === 'inline-block' || display === 'inline-table' || display === 'none' || display === 'table-column' || display === 'table-column-group') {
      return '';
    } else if (display === 'table-cell') {
      return '\t';
    } else {
      var elChunks = [];
      innerRichText(el, elChunks);
      if (elChunks && elChunks.length > 0) {
        var marginBottom = (0, _dom.getPixelStyleProperty)(style, 'margin-bottom');
        var fontSize = (0, _dom.getPixelStyleProperty)(style, 'font-size');
        if (marginBottom >= fontSize / 2) return '\n\n';else return '\n';
      } else return '';
    }
  }
}

function elementHasNonIgnoredSiblingElement(el) {
  var sibling = el.nextSibling;
  while (sibling) {
    if (ignoredNode(sibling) || sibling.nodeName === 'BR') {
      sibling = sibling.nextSibling;
    } else if (sibling.nodeType === Node.ELEMENT_NODE) {
      return true;
    } else {
      sibling = sibling.nextSibling;
    }
  }
  return false;
}

/**
 * The text element portion of Aryeh Gregor's aborted innerText proposal. See
 * https://rawgit.com/timdown/rangy/master/fiddlings/spec/innerText.htm.
 *
 * Does not apply any CSS text-transform instructions, but does warn about them to the console.
 *
 * @param node The text node.
 * @param nodeStyle The style of the node containing the text node.
 * @param chunks The array of rich text chunks to append the text node data to.
 * @param nodeAttributes The rich text attributes of the containing node.
 * @param trailingSpace Whether the previous text node had a trailing space.
 */
function innerRichTextForTextNode(node, nodeStyle, chunks, nodeAttributes, trailingSpace) {
  if (_lodash2['default'].isUndefined(trailingSpace)) trailingSpace = false;

  var inSet = function inSet(c, setString) {
    return setString.indexOf(c) > -1;
  };

  var advanceUntilNotInSet = function advanceUntilNotInSet(data, position, setString) {
    var consume = true;
    while (consume && position < data.length) {
      if (!inSet(data.charAt(position), setString)) {
        consume = false;
      } else {
        position++;
      }
    }
    return position;
  };

  var lastChunk = chunks.length > 0 ? chunks[chunks.length - 1] : null;
  // child is text, step #2
  var data = node.data;
  // child is text, step #3
  var whitespace = nodeStyle.getPropertyValue('white-space');
  var whitespaceCollapse = whitespace === 'normal' || whitespace === 'nowrap' || whitespace === 'pre-line';
  if (whitespaceCollapse) {
    // child is text, step #4 - 1
    var whitespaceChars = whitespace === 'normal' || whitespace === 'nowrap' ? SPACE_CHARS : SPACE_CHARS_EXCEPT_LF;
    // child is text, step #4 - 2
    var position = 0;
    // child is text, step #4 - 3
    var newData = '';
    // child is text, step #4 - 4
    while (position < data.length) {
      var c = data.charAt(position);
      if (whitespaceChars.indexOf(c) > -1) {
        // child is text, step #4 - 4 - 1
        newData += ' ';
        position++;
        position = advanceUntilNotInSet(data, position, whitespaceChars);
      } else if (c === '\n') {
        // child is text, step #4 - 4 - 2
        if (newData.length > 0 && newData.charAt(newData.length - 1) === ' ') {
          newData = newData.slice(0, newData.length - 1);
        }
        newData += '\n';
        position++;
        position = advanceUntilNotInSet(data, position, whitespaceChars);
      } else {
        // child is text, step #4 - 4 - 3
        newData += c;
        position++;
      }
    }
    // child is text, step #4 - 5
    data = newData;
  }
  // child is text, step #5
  if (trailingSpace && (data.length === 0 || !inSet(data.charAt(0), SPACE_CHARS)) && lastChunk) {
    lastChunk.text += ' ';
  }
  // child is text, step #6
  if ((!lastChunk || lastChunk.text.length === 0 || inSet(lastChunk.text.charAt(lastChunk.text.length - 1), SPACE_CHARS)) && (data.length > 0 && data.charAt(0) === ' ') && whitespaceCollapse) {
    data = data.slice(1);
  }
  // child is text, step #7
  if (whitespaceCollapse && (data.length > 0 && data.charAt(data.length - 1) === ' ')) {
    data = data.slice(0, data.length - 1);
    trailingSpace = true;
  } else {
    trailingSpace = false;
  }

  // child is text, step #8
  // ignore text-transform
  var textTransform = nodeStyle.getPropertyValue('text-transform');
  if (textTransform !== 'none' && textTransform !== 'normal') {
    console.warn('Unsupported text-transform ' + textTransform + ' during HTML parsing, ignoring.');
  }

  // child is text, step #9
  if (data.length > 0) {
    chunks.push({
      text: data,
      attrs: nodeAttributes
    });
  }
  return trailingSpace;
}

/**
 * Writes the innerRichText chunks of a node. Logic from Aryeh Gregor's aborted innerText proposal.
 * See https://rawgit.com/timdown/rangy/master/fiddlings/spec/innerText.htm#append-the-plaintext.
 *
 * @param {Node} node The context node for which to obtain the rich text.
 * @param {Array} chunks The array to which parsed rich text chunks (with attributes) should be pushed.
 * @param {boolean} [trailingSpace] Whether the previous node had a trailing space.
 */
function innerRichText(node, chunks, trailingSpace) {
  var nodeAttributes = attributesForElement(node);
  for (var i = 0; i < node.childNodes.length; i++) {
    var child = node.childNodes[i];
    var ignored = ignoredNode(child);
    if (!ignored && child.nodeType === Node.TEXT_NODE) {
      // child is text, step #1
      var nodeStyle = getComputedStyle(node, null);
      if (nodeStyle.getPropertyValue('visibility') !== 'hidden') {
        trailingSpace = innerRichTextForTextNode(child, nodeStyle, chunks, nodeAttributes, trailingSpace);
      }
    } else if (!ignored && child.nodeType === Node.ELEMENT_NODE) {
      // child is element, step #1
      var lastChunk = chunks.length > 0 ? chunks[chunks.length - 1] : null;
      if (lastChunk && lastChunk.text.length > 0 && lastChunk.text.charAt(lastChunk.text.length - 1) !== '\n') {
        var leadingWhitespace = elementLeadingWhitespace(child);
        if (leadingWhitespace && leadingWhitespace.length > 0) {
          lastChunk.text += leadingWhitespace;
          trailingSpace = false;
        }
      }

      // child is element, step #2
      trailingSpace = innerRichText(child, chunks, trailingSpace);

      // child is element, step #3 (with addition to special-case br tag)
      if (child.nodeName === 'BR') {
        chunks.push({
          text: '\n',
          attrs: nodeAttributes
        });
        trailingSpace = false;
      } else if (elementHasNonIgnoredSiblingElement(child)) {
        // we ignore BR's here too, special cased above
        // a sibling node that is not an ignored node, so we need our trailing whitespace
        lastChunk = chunks.length > 0 ? chunks[chunks.length - 1] : null;
        var trailingWhitespace = elementTrailingWhitespace(child);
        if (trailingWhitespace && trailingWhitespace.length > 0) {
          if (!lastChunk) {
            lastChunk = {
              text: '',
              attrs: nodeAttributes
            };
            chunks.push(lastChunk);
          }
          lastChunk.text += trailingWhitespace;
          trailingSpace = false;
        }
      }
    }
  }
  return trailingSpace;
}

/**
 * Parses HTML into chunks of rich text.
 *
 * Argh, getting the appropriate text from an HTML DOM tree is not simple. See
 * http://perfectionkills.com/the-poor-misunderstood-innerText/.
 *
 * Implement Aryeh Gregor's aborted innerText proposal: https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
 * and https://rawgit.com/timdown/rangy/master/fiddlings/spec/innerText.htm, which works quite well (see the
 * test cases for this module).
 *
 * The innerText algorithm presented by Gregor was slightly modified to return chunks of styled rich text
 * (text with attributes) based on the DOM nodes and their computed styles.
 *
 * @param {string} html The html String to parse.
 * @param {Node} tempContainer An invisible container within the document that can hold the parsed HTML temporarily
 *   so that the browser can compute the CSS styles. This is a hack but there doesn't seem to be a better way of doing
 *   this.
 */

function parseHtml(html, tempContainer) {
  var parser = new DOMParser();
  var doc = parser.parseFromString(html, 'text/html');

  var chunks = [];

  // IE does not give us an empty body, only a null one, shortcut the subsequent logic in this case
  if (!doc.body) {
    return chunks;
  }

  insertIntoContainer(doc.body, tempContainer);

  try {
    innerRichText(tempContainer, chunks);
  } finally {
    (0, _dom.emptyNode)(tempContainer);
  }

  return chunks;
}

module.exports = exports['default'];