text: var log = require('./log');
  var punycode = require('punycode');
  var NodeContainer = require('./nodecontainer');
  var TextContainer = require('./textcontainer');
  var PseudoElementContainer = require('./pseudoelementcontainer');
  var FontMetrics = require('./fontmetrics');
  var Color = require('./color');
  var StackingContext = require('./stackingcontext');
  var utils = require('./utils');
  var bind = utils.bind;
  var getBounds = utils.getBounds;
  var parseBackgrounds = utils.parseBackgrounds;
  var offsetBounds = utils.offsetBounds;
  
  function NodeParser(element, renderer, support, imageLoader, options) {
      log("Starting NodeParser");
      this.renderer = renderer;
      this.options = options;
      this.range = null;
      this.support = support;
      this.renderQueue = [];
      this.stack = new StackingContext(true, 1, element.ownerDocument, null);
      var parent = new NodeContainer(element, null);
      if (options.background) {
          renderer.rectangle(0, 0, renderer.width, renderer.height, new Color(options.background));
      }
      if (element === element.ownerDocument.documentElement) {
          // http://www.w3.org/TR/css3-background/#special-backgrounds
          var canvasBackground = new NodeContainer(parent.color('backgroundColor').isTransparent() ? element.ownerDocument.body : element.ownerDocument.documentElement, null);
          renderer.rectangle(0, 0, renderer.width, renderer.height, canvasBackground.color('backgroundColor'));
      }
      parent.visibile = parent.isElementVisible();
      this.createPseudoHideStyles(element.ownerDocument);
      this.disableAnimations(element.ownerDocument);
      this.nodes = flatten([parent].concat(this.getChildren(parent)).filter(function(container) {
          return container.visible = container.isElementVisible();
      }).map(this.getPseudoElements, this));
      this.fontMetrics = new FontMetrics();
      log("Fetched nodes, total:", this.nodes.length);
      log("Calculate overflow clips");
      this.calculateOverflowClips();
      log("Start fetching images");
      this.images = imageLoader.fetch(this.nodes.filter(isElement));
      this.ready = this.images.ready.then(bind(function() {
          log("Images loaded, starting parsing");
          log("Creating stacking contexts");
          this.createStackingContexts();
          log("Sorting stacking contexts");
          this.sortStackingContexts(this.stack);
          this.parse(this.stack);
          log("Render queue created with " + this.renderQueue.length + " items");
          return new Promise(bind(function(resolve) {
              if (!options.async) {
                  this.renderQueue.forEach(this.paint, this);
                  resolve();
              } else if (typeof(options.async) === "function") {
                  options.async.call(this, this.renderQueue, resolve);
              } else if (this.renderQueue.length > 0){
                  this.renderIndex = 0;
                  this.asyncRenderer(this.renderQueue, resolve);
              } else {
                  resolve();
              }
          }, this));
      }, this));
  }
  
  NodeParser.prototype.calculateOverflowClips = function() {
      this.nodes.forEach(function(container) {
          if (isElement(container)) {
              if (isPseudoElement(container)) {
                  container.appendToDOM();
              }
              container.borders = this.parseBorders(container);
              var clip = (container.css('overflow') === "hidden") ? [container.borders.clip] : [];
              var cssClip = container.parseClip();
              if (cssClip && ["absolute", "fixed"].indexOf(container.css('position')) !== -1) {
                  clip.push([["rect",
                          container.bounds.left + cssClip.left,
                          container.bounds.top + cssClip.top,
                          cssClip.right - cssClip.left,
                          cssClip.bottom - cssClip.top
                  ]]);
              }
              container.clip = hasParentClip(container) ? container.parent.clip.concat(clip) : clip;
              container.backgroundClip = (container.css('overflow') !== "hidden") ? container.clip.concat([container.borders.clip]) : container.clip;
              if (isPseudoElement(container)) {
                  container.cleanDOM();
              }
          } else if (isTextNode(container)) {
              container.clip = hasParentClip(container) ? container.parent.clip : [];
          }
          if (!isPseudoElement(container)) {
              container.bounds = null;
          }
      }, this);
  };
  
  function hasParentClip(container) {
      return container.parent && container.parent.clip.length;
  }
  
  NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) {
      asyncTimer = asyncTimer || Date.now();
      this.paint(queue[this.renderIndex++]);
      if (queue.length === this.renderIndex) {
          resolve();
      } else if (asyncTimer + 20 > Date.now()) {
          this.asyncRenderer(queue, resolve, asyncTimer);
      } else {
          setTimeout(bind(function() {
              this.asyncRenderer(queue, resolve);
          }, this), 0);
      }
  };
  
  NodeParser.prototype.createPseudoHideStyles = function(document) {
      this.createStyles(document, '.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + ':before { content: "" !important; display: none !important; }' +
          '.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER + ':after { content: "" !important; display: none !important; }');
  };
  
  NodeParser.prototype.disableAnimations = function(document) {
      this.createStyles(document, '* { -webkit-animation: none !important; -moz-animation: none !important; -o-animation: none !important; animation: none !important; ' +
          '-webkit-transition: none !important; -moz-transition: none !important; -o-transition: none !important; transition: none !important;}');
  };
  
  NodeParser.prototype.createStyles = function(document, styles) {
      var hidePseudoElements = document.createElement('style');
      hidePseudoElements.innerHTML = styles;
      document.body.appendChild(hidePseudoElements);
  };
  
  NodeParser.prototype.getPseudoElements = function(container) {
      var nodes = [[container]];
      if (container.node.nodeType === Node.ELEMENT_NODE) {
          var before = this.getPseudoElement(container, ":before");
          var after = this.getPseudoElement(container, ":after");
  
          if (before) {
              nodes.push(before);
          }
  
          if (after) {
              nodes.push(after);
          }
      }
      return flatten(nodes);
  };
  
  function toCamelCase(str) {
      return str.replace(/(\-[a-z])/g, function(match){
          return match.toUpperCase().replace('-','');
      });
  }
  
  NodeParser.prototype.getPseudoElement = function(container, type) {
      var style = container.computedStyle(type);
      if(!style || !style.content || style.content === "none" || style.content === "-moz-alt-content" || style.display === "none") {
          return null;
      }
  
      var content = stripQuotes(style.content);
      var isImage = content.substr(0, 3) === 'url';
      var pseudoNode = document.createElement(isImage ? 'img' : 'html2canvaspseudoelement');
      var pseudoContainer = new PseudoElementContainer(pseudoNode, container, type);
  
      for (var i = style.length-1; i >= 0; i--) {
          var property = toCamelCase(style.item(i));
          pseudoNode.style[property] = style[property];
      }
  
      pseudoNode.className = PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + " " + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER;
  
      if (isImage) {
          pseudoNode.src = parseBackgrounds(content)[0].args[0];
          return [pseudoContainer];
      } else {
          var text = document.createTextNode(content);
          pseudoNode.appendChild(text);
          return [pseudoContainer, new TextContainer(text, pseudoContainer)];
      }
  };
  
  
  NodeParser.prototype.getChildren = function(parentContainer) {
      return flatten([].filter.call(parentContainer.node.childNodes, renderableNode).map(function(node) {
          var container = [node.nodeType === Node.TEXT_NODE ? new TextContainer(node, parentContainer) : new NodeContainer(node, parentContainer)].filter(nonIgnoredElement);
          return node.nodeType === Node.ELEMENT_NODE && container.length && node.tagName !== "TEXTAREA" ? (container[0].isElementVisible() ? container.concat(this.getChildren(container[0])) : []) : container;
      }, this));
  };
  
  NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) {
      var stack = new StackingContext(hasOwnStacking, container.getOpacity(), container.node, container.parent);
      container.cloneTo(stack);
      var parentStack = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack;
      parentStack.contexts.push(stack);
      container.stack = stack;
  };
  
  NodeParser.prototype.createStackingContexts = function() {
      this.nodes.forEach(function(container) {
          if (isElement(container) && (this.isRootElement(container) || hasOpacity(container) || isPositionedForStacking(container) || this.isBodyWithTransparentRoot(container) || container.hasTransform())) {
              this.newStackingContext(container, true);
          } else if (isElement(container) && ((isPositioned(container) && zIndex0(container)) || isInlineBlock(container) || isFloating(container))) {
              this.newStackingContext(container, false);
          } else {
              container.assignStack(container.parent.stack);
          }
      }, this);
  };
  
  NodeParser.prototype.isBodyWithTransparentRoot = function(container) {
      return container.node.nodeName === "BODY" && container.parent.color('backgroundColor').isTransparent();
  };
  
  NodeParser.prototype.isRootElement = function(container) {
      return container.parent === null;
  };
  
  NodeParser.prototype.sortStackingContexts = function(stack) {
      stack.contexts.sort(zIndexSort(stack.contexts.slice(0)));
      stack.contexts.forEach(this.sortStackingContexts, this);
  };
  
  NodeParser.prototype.parseTextBounds = function(container) {
      return function(text, index, textList) {
          if (container.parent.css("textDecoration").substr(0, 4) !== "none" || text.trim().length !== 0) {
              if (this.support.rangeBounds && !container.parent.hasTransform()) {
                  var offset = textList.slice(0, index).join("").length;
                  return this.getRangeBounds(container.node, offset, text.length);
              } else if (container.node && typeof(container.node.data) === "string") {
                  var replacementNode = container.node.splitText(text.length);
                  var bounds = this.getWrapperBounds(container.node, container.parent.hasTransform());
                  container.node = replacementNode;
                  return bounds;
              }
          } else if(!this.support.rangeBounds || container.parent.hasTransform()){
              container.node = container.node.splitText(text.length);
          }
          return {};
      };
  };
  
  NodeParser.prototype.getWrapperBounds = function(node, transform) {
      var wrapper = node.ownerDocument.createElement('html2canvaswrapper');
      var parent = node.parentNode,
          backupText = node.cloneNode(true);
  
      wrapper.appendChild(node.cloneNode(true));
      parent.replaceChild(wrapper, node);
      var bounds = transform ? offsetBounds(wrapper) : getBounds(wrapper);
      parent.replaceChild(backupText, wrapper);
      return bounds;
  };
  
  NodeParser.prototype.getRangeBounds = function(node, offset, length) {
      var range = this.range || (this.range = node.ownerDocument.createRange());
      range.setStart(node, offset);
      range.setEnd(node, offset + length);
      return range.getBoundingClientRect();
  };
  
  function ClearTransform() {}
  
  NodeParser.prototype.parse = function(stack) {
      // http://www.w3.org/TR/CSS21/visuren.html#z-index
      var negativeZindex = stack.contexts.filter(negativeZIndex); // 2. the child stacking contexts with negative stack levels (most negative first).
      var descendantElements = stack.children.filter(isElement);
      var descendantNonFloats = descendantElements.filter(not(isFloating));
      var nonInlineNonPositionedDescendants = descendantNonFloats.filter(not(isPositioned)).filter(not(inlineLevel)); // 3 the in-flow, non-inline-level, non-positioned descendants.
      var nonPositionedFloats = descendantElements.filter(not(isPositioned)).filter(isFloating); // 4. the non-positioned floats.
      var inFlow = descendantNonFloats.filter(not(isPositioned)).filter(inlineLevel); // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
      var stackLevel0 = stack.contexts.concat(descendantNonFloats.filter(isPositioned)).filter(zIndex0); // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
      var text = stack.children.filter(isTextNode).filter(hasText);
      var positiveZindex = stack.contexts.filter(positiveZIndex); // 7. the child stacking contexts with positive stack levels (least positive first).
      negativeZindex.concat(nonInlineNonPositionedDescendants).concat(nonPositionedFloats)
          .concat(inFlow).concat(stackLevel0).concat(text).concat(positiveZindex).forEach(function(container) {
              this.renderQueue.push(container);
              if (isStackingContext(container)) {
                  this.parse(container);
                  this.renderQueue.push(new ClearTransform());
              }
          }, this);
  };
  
  NodeParser.prototype.paint = function(container) {
      try {
          if (container instanceof ClearTransform) {
              this.renderer.ctx.restore();
          } else if (isTextNode(container)) {
              if (isPseudoElement(container.parent)) {
                  container.parent.appendToDOM();
              }
              this.paintText(container);
              if (isPseudoElement(container.parent)) {
                  container.parent.cleanDOM();
              }
          } else {
              this.paintNode(container);
          }
      } catch(e) {
          log(e);
          if (this.options.strict) {
              throw e;
          }
      }
  };
  
  NodeParser.prototype.paintNode = function(container) {
      if (isStackingContext(container)) {
          this.renderer.setOpacity(container.opacity);
          this.renderer.ctx.save();
          if (container.hasTransform()) {
              this.renderer.setTransform(container.parseTransform());
          }
      }
  
      if (container.node.nodeName === "INPUT" && container.node.type === "checkbox") {
          this.paintCheckbox(container);
      } else if (container.node.nodeName === "INPUT" && container.node.type === "radio") {
          this.paintRadio(container);
      } else {
          this.paintElement(container);
      }
  };
  
  NodeParser.prototype.paintElement = function(container) {
      var bounds = container.parseBounds();
      this.renderer.clip(container.backgroundClip, function() {
          this.renderer.renderBackground(container, bounds, container.borders.borders.map(getWidth));
      }, this);
  
      this.renderer.clip(container.clip, function() {
          this.renderer.renderBorders(container.borders.borders);
      }, this);
  
      this.renderer.clip(container.backgroundClip, function() {
          switch (container.node.nodeName) {
          case "svg":
          case "IFRAME":
              var imgContainer = this.images.get(container.node);
              if (imgContainer) {
                  this.renderer.renderImage(container, bounds, container.borders, imgContainer);
              } else {
                  log("Error loading <" + container.node.nodeName + ">", container.node);
              }
              break;
          case "IMG":
              var imageContainer = this.images.get(container.node.src);
              if (imageContainer) {
                  this.renderer.renderImage(container, bounds, container.borders, imageContainer);
              } else {
                  log("Error loading <img>", container.node.src);
              }
              break;
          case "VIDEO":
              var videoContainer = this.images.getVideo(container.node.videoIndex);
              if (videoContainer) {
                  this.renderer.renderImage(container, bounds, container.borders, videoContainer);
              } else {
                  log("Error loading <video>", container.node.src);
              }
              break;
          case "CANVAS":
              this.renderer.renderImage(container, bounds, container.borders, {image: container.node});
              break;
          case "SELECT":
          case "INPUT":
          case "TEXTAREA":
              this.paintFormValue(container);
              break;
          }
      }, this);
  };
  
  NodeParser.prototype.paintCheckbox = function(container) {
      var b = container.parseBounds();
  
      var size = Math.min(b.width, b.height);
      var bounds = {width: size - 1, height: size - 1, top: b.top, left: b.left};
      var r = [3, 3];
      var radius = [r, r, r, r];
      var borders = [1,1,1,1].map(function(w) {
          return {color: new Color('#A5A5A5'), width: w};
      });
  
      var borderPoints = calculateCurvePoints(bounds, radius, borders);
  
      this.renderer.clip(container.backgroundClip, function() {
          this.renderer.rectangle(bounds.left + 1, bounds.top + 1, bounds.width - 2, bounds.height - 2, new Color("#DEDEDE"));
          this.renderer.renderBorders(calculateBorders(borders, bounds, borderPoints, radius));
          if (container.node.checked) {
              this.renderer.font(new Color('#424242'), 'normal', 'normal', 'bold', (size - 3) + "px", 'arial');
              this.renderer.text("\u2714", bounds.left + size / 6, bounds.top + size - 1);
          }
      }, this);
  };
  
  NodeParser.prototype.paintRadio = function(container) {
      var bounds = container.parseBounds();
  
      var size = Math.min(bounds.width, bounds.height) - 2;
  
      this.renderer.clip(container.backgroundClip, function() {
          this.renderer.circleStroke(bounds.left + 1, bounds.top + 1, size, new Color('#DEDEDE'), 1, new Color('#A5A5A5'));
          if (container.node.checked) {
              this.renderer.circle(Math.ceil(bounds.left + size / 4) + 1, Math.ceil(bounds.top + size / 4) + 1, Math.floor(size / 2), new Color('#424242'));
          }
      }, this);
  };
  
  NodeParser.prototype.paintFormValue = function(container) {
      var value = container.getValue();
      if (value.length > 0) {
          var document = container.node.ownerDocument;
          var wrapper = document.createElement('html2canvaswrapper');
          var properties = ['lineHeight', 'textAlign', 'fontFamily', 'fontWeight', 'fontSize', 'color',
              'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom',
              'width', 'height', 'borderLeftStyle', 'borderTopStyle', 'borderLeftWidth', 'borderTopWidth',
              'boxSizing', 'whiteSpace', 'wordWrap'];
  
          properties.forEach(function(property) {
              try {
                  wrapper.style[property] = container.css(property);
              } catch(e) {
                  // Older IE has issues with "border"
                  log("html2canvas: Parse: Exception caught in renderFormValue: " + e.message);
              }
          });
          var bounds = container.parseBounds();
          wrapper.style.position = "fixed";
          wrapper.style.left = bounds.left + "px";
          wrapper.style.top = bounds.top + "px";
          wrapper.textContent = value;
          document.body.appendChild(wrapper);
          this.paintText(new TextContainer(wrapper.firstChild, container));
          document.body.removeChild(wrapper);
      }
  };
  
  NodeParser.prototype.paintText = function(container) {
      container.applyTextTransform();
      var characters = punycode.ucs2.decode(container.node.data);
      var textList = (!this.options.letterRendering || noLetterSpacing(container)) && !hasUnicode(container.node.data) ? getWords(characters) : characters.map(function(character) {
          return punycode.ucs2.encode([character]);
      });
  
      var weight = container.parent.fontWeight();
      var size = container.parent.css('fontSize');
      var family = container.parent.css('fontFamily');
      var shadows = container.parent.parseTextShadows();
  
      this.renderer.font(container.parent.color('color'), container.parent.css('fontStyle'), container.parent.css('fontVariant'), weight, size, family);
      if (shadows.length) {
          // TODO: support multiple text shadows
          this.renderer.fontShadow(shadows[0].color, shadows[0].offsetX, shadows[0].offsetY, shadows[0].blur);
      } else {
          this.renderer.clearShadow();
      }
  
      this.renderer.clip(container.parent.clip, function() {
          textList.map(this.parseTextBounds(container), this).forEach(function(bounds, index) {
              if (bounds) {
                  this.renderer.text(textList[index], bounds.left, bounds.bottom);
                  this.renderTextDecoration(container.parent, bounds, this.fontMetrics.getMetrics(family, size));
              }
          }, this);
      }, this);
  };
  
  NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics) {
      switch(container.css("textDecoration").split(" ")[0]) {
      case "underline":
          // Draws a line at the baseline of the font
          // TODO As some browsers display the line as more than 1px if the font-size is big, need to take that into account both in position and size
          this.renderer.rectangle(bounds.left, Math.round(bounds.top + metrics.baseline + metrics.lineWidth), bounds.width, 1, container.color("color"));
          break;
      case "overline":
          this.renderer.rectangle(bounds.left, Math.round(bounds.top), bounds.width, 1, container.color("color"));
          break;
      case "line-through":
          // TODO try and find exact position for line-through
          this.renderer.rectangle(bounds.left, Math.ceil(bounds.top + metrics.middle + metrics.lineWidth), bounds.width, 1, container.color("color"));
          break;
      }
  };
  
  var borderColorTransforms = {
      inset: [
          ["darken", 0.60],
          ["darken", 0.10],
          ["darken", 0.10],
          ["darken", 0.60]
      ]
  };
  
  NodeParser.prototype.parseBorders = function(container) {
      var nodeBounds = container.parseBounds();
      var radius = getBorderRadiusData(container);
      var borders = ["Top", "Right", "Bottom", "Left"].map(function(side, index) {
          var style = container.css('border' + side + 'Style');
          var color = container.color('border' + side + 'Color');
          if (style === "inset" && color.isBlack()) {
              color = new Color([255, 255, 255, color.a]); // this is wrong, but
          }
          var colorTransform = borderColorTransforms[style] ? borderColorTransforms[style][index] : null;
          return {
              width: container.cssInt('border' + side + 'Width'),
              color: colorTransform ? color[colorTransform[0]](colorTransform[1]) : color,
              args: null
          };
      });
      var borderPoints = calculateCurvePoints(nodeBounds, radius, borders);
  
      return {
          clip: this.parseBackgroundClip(container, borderPoints, borders, radius, nodeBounds),
          borders: calculateBorders(borders, nodeBounds, borderPoints, radius)
      };
  };
  
  function calculateBorders(borders, nodeBounds, borderPoints, radius) {
      return borders.map(function(border, borderSide) {
          if (border.width > 0) {
              var bx = nodeBounds.left;
              var by = nodeBounds.top;
              var bw = nodeBounds.width;
              var bh = nodeBounds.height - (borders[2].width);
  
              switch(borderSide) {
              case 0:
                  // top border
                  bh = borders[0].width;
                  border.args = drawSide({
                          c1: [bx, by],
                          c2: [bx + bw, by],
                          c3: [bx + bw - borders[1].width, by + bh],
                          c4: [bx + borders[3].width, by + bh]
                      }, radius[0], radius[1],
                      borderPoints.topLeftOuter, borderPoints.topLeftInner, borderPoints.topRightOuter, borderPoints.topRightInner);
                  break;
              case 1:
                  // right border
                  bx = nodeBounds.left + nodeBounds.width - (borders[1].width);
                  bw = borders[1].width;
  
                  border.args = drawSide({
                          c1: [bx + bw, by],
                          c2: [bx + bw, by + bh + borders[2].width],
                          c3: [bx, by + bh],
                          c4: [bx, by + borders[0].width]
                      }, radius[1], radius[2],
                      borderPoints.topRightOuter, borderPoints.topRightInner, borderPoints.bottomRightOuter, borderPoints.bottomRightInner);
                  break;
              case 2:
                  // bottom border
                  by = (by + nodeBounds.height) - (borders[2].width);
                  bh = borders[2].width;
                  border.args = drawSide({
                          c1: [bx + bw, by + bh],
                          c2: [bx, by + bh],
                          c3: [bx + borders[3].width, by],
                          c4: [bx + bw - borders[3].width, by]
                      }, radius[2], radius[3],
                      borderPoints.bottomRightOuter, borderPoints.bottomRightInner, borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner);
                  break;
              case 3:
                  // left border
                  bw = borders[3].width;
                  border.args = drawSide({
                          c1: [bx, by + bh + borders[2].width],
                          c2: [bx, by],
                          c3: [bx + bw, by + borders[0].width],
                          c4: [bx + bw, by + bh]
                      }, radius[3], radius[0],
                      borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner, borderPoints.topLeftOuter, borderPoints.topLeftInner);
                  break;
              }
          }
          return border;
      });
  }
  
  NodeParser.prototype.parseBackgroundClip = function(container, borderPoints, borders, radius, bounds) {
      var backgroundClip = container.css('backgroundClip'),
          borderArgs = [];
  
      switch(backgroundClip) {
      case "content-box":
      case "padding-box":
          parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftInner, borderPoints.topRightInner, bounds.left + borders[3].width, bounds.top + borders[0].width);
          parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightInner, borderPoints.bottomRightInner, bounds.left + bounds.width - borders[1].width, bounds.top + borders[0].width);
          parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightInner, borderPoints.bottomLeftInner, bounds.left + bounds.width - borders[1].width, bounds.top + bounds.height - borders[2].width);
          parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftInner, borderPoints.topLeftInner, bounds.left + borders[3].width, bounds.top + bounds.height - borders[2].width);
          break;
  
      default:
          parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftOuter, borderPoints.topRightOuter, bounds.left, bounds.top);
          parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightOuter, borderPoints.bottomRightOuter, bounds.left + bounds.width, bounds.top);
          parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightOuter, borderPoints.bottomLeftOuter, bounds.left + bounds.width, bounds.top + bounds.height);
          parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftOuter, borderPoints.topLeftOuter, bounds.left, bounds.top + bounds.height);
          break;
      }
  
      return borderArgs;
  };
  
  function getCurvePoints(x, y, r1, r2) {
      var kappa = 4 * ((Math.sqrt(2) - 1) / 3);
      var ox = (r1) * kappa, // control point offset horizontal
          oy = (r2) * kappa, // control point offset vertical
          xm = x + r1, // x-middle
          ym = y + r2; // y-middle
      return {
          topLeft: bezierCurve({x: x, y: ym}, {x: x, y: ym - oy}, {x: xm - ox, y: y}, {x: xm, y: y}),
          topRight: bezierCurve({x: x, y: y}, {x: x + ox,y: y}, {x: xm, y: ym - oy}, {x: xm, y: ym}),
          bottomRight: bezierCurve({x: xm, y: y}, {x: xm, y: y + oy}, {x: x + ox, y: ym}, {x: x, y: ym}),
          bottomLeft: bezierCurve({x: xm, y: ym}, {x: xm - ox, y: ym}, {x: x, y: y + oy}, {x: x, y:y})
      };
  }
  
  function calculateCurvePoints(bounds, borderRadius, borders) {
      var x = bounds.left,
          y = bounds.top,
          width = bounds.width,
          height = bounds.height,
  
          tlh = borderRadius[0][0] < width / 2 ? borderRadius[0][0] : width / 2,
          tlv = borderRadius[0][1] < height / 2 ? borderRadius[0][1] : height / 2,
          trh = borderRadius[1][0] < width / 2 ? borderRadius[1][0] : width / 2,
          trv = borderRadius[1][1] < height / 2 ? borderRadius[1][1] : height / 2,
          brh = borderRadius[2][0] < width / 2 ? borderRadius[2][0] : width / 2,
          brv = borderRadius[2][1] < height / 2 ? borderRadius[2][1] : height / 2,
          blh = borderRadius[3][0] < width / 2 ? borderRadius[3][0] : width / 2,
          blv = borderRadius[3][1] < height / 2 ? borderRadius[3][1] : height / 2;
  
      var topWidth = width - trh,
          rightHeight = height - brv,
          bottomWidth = width - brh,
          leftHeight = height - blv;
  
      return {
          topLeftOuter: getCurvePoints(x, y, tlh, tlv).topLeft.subdivide(0.5),
          topLeftInner: getCurvePoints(x + borders[3].width, y + borders[0].width, Math.max(0, tlh - borders[3].width), Math.max(0, tlv - borders[0].width)).topLeft.subdivide(0.5),
          topRightOuter: getCurvePoints(x + topWidth, y, trh, trv).topRight.subdivide(0.5),
          topRightInner: getCurvePoints(x + Math.min(topWidth, width + borders[3].width), y + borders[0].width, (topWidth > width + borders[3].width) ? 0 :trh - borders[3].width, trv - borders[0].width).topRight.subdivide(0.5),
          bottomRightOuter: getCurvePoints(x + bottomWidth, y + rightHeight, brh, brv).bottomRight.subdivide(0.5),
          bottomRightInner: getCurvePoints(x + Math.min(bottomWidth, width - borders[3].width), y + Math.min(rightHeight, height + borders[0].width), Math.max(0, brh - borders[1].width),  brv - borders[2].width).bottomRight.subdivide(0.5),
          bottomLeftOuter: getCurvePoints(x, y + leftHeight, blh, blv).bottomLeft.subdivide(0.5),
          bottomLeftInner: getCurvePoints(x + borders[3].width, y + leftHeight, Math.max(0, blh - borders[3].width), blv - borders[2].width).bottomLeft.subdivide(0.5)
      };
  }
  
  function bezierCurve(start, startControl, endControl, end) {
      var lerp = function (a, b, t) {
          return {
              x: a.x + (b.x - a.x) * t,
              y: a.y + (b.y - a.y) * t
          };
      };
  
      return {
          start: start,
          startControl: startControl,
          endControl: endControl,
          end: end,
          subdivide: function(t) {
              var ab = lerp(start, startControl, t),
                  bc = lerp(startControl, endControl, t),
                  cd = lerp(endControl, end, t),
                  abbc = lerp(ab, bc, t),
                  bccd = lerp(bc, cd, t),
                  dest = lerp(abbc, bccd, t);
              return [bezierCurve(start, ab, abbc, dest), bezierCurve(dest, bccd, cd, end)];
          },
          curveTo: function(borderArgs) {
              borderArgs.push(["bezierCurve", startControl.x, startControl.y, endControl.x, endControl.y, end.x, end.y]);
          },
          curveToReversed: function(borderArgs) {
              borderArgs.push(["bezierCurve", endControl.x, endControl.y, startControl.x, startControl.y, start.x, start.y]);
          }
      };
  }
  
  function drawSide(borderData, radius1, radius2, outer1, inner1, outer2, inner2) {
      var borderArgs = [];
  
      if (radius1[0] > 0 || radius1[1] > 0) {
          borderArgs.push(["line", outer1[1].start.x, outer1[1].start.y]);
          outer1[1].curveTo(borderArgs);
      } else {
          borderArgs.push([ "line", borderData.c1[0], borderData.c1[1]]);
      }
  
      if (radius2[0] > 0 || radius2[1] > 0) {
          borderArgs.push(["line", outer2[0].start.x, outer2[0].start.y]);
          outer2[0].curveTo(borderArgs);
          borderArgs.push(["line", inner2[0].end.x, inner2[0].end.y]);
          inner2[0].curveToReversed(borderArgs);
      } else {
          borderArgs.push(["line", borderData.c2[0], borderData.c2[1]]);
          borderArgs.push(["line", borderData.c3[0], borderData.c3[1]]);
      }
  
      if (radius1[0] > 0 || radius1[1] > 0) {
          borderArgs.push(["line", inner1[1].end.x, inner1[1].end.y]);
          inner1[1].curveToReversed(borderArgs);
      } else {
          borderArgs.push(["line", borderData.c4[0], borderData.c4[1]]);
      }
  
      return borderArgs;
  }
  
  function parseCorner(borderArgs, radius1, radius2, corner1, corner2, x, y) {
      if (radius1[0] > 0 || radius1[1] > 0) {
          borderArgs.push(["line", corner1[0].start.x, corner1[0].start.y]);
          corner1[0].curveTo(borderArgs);
          corner1[1].curveTo(borderArgs);
      } else {
          borderArgs.push(["line", x, y]);
      }
  
      if (radius2[0] > 0 || radius2[1] > 0) {
          borderArgs.push(["line", corner2[0].start.x, corner2[0].start.y]);
      }
  }
  
  function negativeZIndex(container) {
      return container.cssInt("zIndex") < 0;
  }
  
  function positiveZIndex(container) {
      return container.cssInt("zIndex") > 0;
  }
  
  function zIndex0(container) {
      return container.cssInt("zIndex") === 0;
  }
  
  function inlineLevel(container) {
      return ["inline", "inline-block", "inline-table"].indexOf(container.css("display")) !== -1;
  }
  
  function isStackingContext(container) {
      return (container instanceof StackingContext);
  }
  
  function hasText(container) {
      return container.node.data.trim().length > 0;
  }
  
  function noLetterSpacing(container) {
      return (/^(normal|none|0px)$/.test(container.parent.css("letterSpacing")));
  }
  
  function getBorderRadiusData(container) {
      return ["TopLeft", "TopRight", "BottomRight", "BottomLeft"].map(function(side) {
          var value = container.css('border' + side + 'Radius');
          var arr = value.split(" ");
          if (arr.length <= 1) {
              arr[1] = arr[0];
          }
          return arr.map(asInt);
      });
  }
  
  function renderableNode(node) {
      return (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE);
  }
  
  function isPositionedForStacking(container) {
      var position = container.css("position");
      var zIndex = (["absolute", "relative", "fixed"].indexOf(position) !== -1) ? container.css("zIndex") : "auto";
      return zIndex !== "auto";
  }
  
  function isPositioned(container) {
      return container.css("position") !== "static";
  }
  
  function isFloating(container) {
      return container.css("float") !== "none";
  }
  
  function isInlineBlock(container) {
      return ["inline-block", "inline-table"].indexOf(container.css("display")) !== -1;
  }
  
  function not(callback) {
      var context = this;
      return function() {
          return !callback.apply(context, arguments);
      };
  }
  
  function isElement(container) {
      return container.node.nodeType === Node.ELEMENT_NODE;
  }
  
  function isPseudoElement(container) {
      return container.isPseudoElement === true;
  }
  
  function isTextNode(container) {
      return container.node.nodeType === Node.TEXT_NODE;
  }
  
  function zIndexSort(contexts) {
      return function(a, b) {
          return (a.cssInt("zIndex") + (contexts.indexOf(a) / contexts.length)) - (b.cssInt("zIndex") + (contexts.indexOf(b) / contexts.length));
      };
  }
  
  function hasOpacity(container) {
      return container.getOpacity() < 1;
  }
  
  function asInt(value) {
      return parseInt(value, 10);
  }
  
  function getWidth(border) {
      return border.width;
  }
  
  function nonIgnoredElement(nodeContainer) {
      return (nodeContainer.node.nodeType !== Node.ELEMENT_NODE || ["SCRIPT", "HEAD", "TITLE", "OBJECT", "BR", "OPTION"].indexOf(nodeContainer.node.nodeName) === -1);
  }
  
  function flatten(arrays) {
      return [].concat.apply([], arrays);
  }
  
  function stripQuotes(content) {
      var first = content.substr(0, 1);
      return (first === content.substr(content.length - 1) && first.match(/'|"/)) ? content.substr(1, content.length - 2) : content;
  }
  
  function getWords(characters) {
      var words = [], i = 0, onWordBoundary = false, word;
      while(characters.length) {
          if (isWordBoundary(characters[i]) === onWordBoundary) {
              word = characters.splice(0, i);
              if (word.length) {
                  words.push(punycode.ucs2.encode(word));
              }
              onWordBoundary =! onWordBoundary;
              i = 0;
          } else {
              i++;
          }
  
          if (i >= characters.length) {
              word = characters.splice(0, i);
              if (word.length) {
                  words.push(punycode.ucs2.encode(word));
              }
          }
      }
      return words;
  }
  
  function isWordBoundary(characterCode) {
      return [
          32, // <space>
          13, // \r
          10, // \n
          9, // \t
          45 // -
      ].indexOf(characterCode) !== -1;
  }
  
  function hasUnicode(string) {
      return (/[^\u0000-\u00ff]/).test(string);
  }
  
  module.exports = NodeParser;
  