docs: function (sc, so, ec, eo) {
              this.sc = sc;
              this.so = so;
              this.ec = ec;
              this.eo = eo;
  
              // nativeRange: get nativeRange from sc, so, ec, eo
              var nativeRange = function () {
                  if (agent.isW3CRangeSupport) {
                      var w3cRange = document.createRange();
                      w3cRange.setStart(sc, so);
                      w3cRange.setEnd(ec, eo);
  
                      return w3cRange;
                  } else {
                      var textRange = pointToTextRange({
                          node: sc,
                          offset: so
                      });
  
                      textRange.setEndPoint('EndToEnd', pointToTextRange({
                          node: ec,
                          offset: eo
                      }));
  
                      return textRange;
                  }
              };
  
              this.getPoints = function () {
                  return {
                      sc: sc,
                      so: so,
                      ec: ec,
                      eo: eo
                  };
              };
  
              this.getStartPoint = function () {
                  return {
                      node: sc,
                      offset: so
                  };
              };
  
              this.getEndPoint = function () {
                  return {
                      node: ec,
                      offset: eo
                  };
              };
  
              /**
              * select update visible range
              */
              this.select = function () {
                  var nativeRng = nativeRange();
                  if (agent.isW3CRangeSupport) {
                      var selection = document.getSelection();
                      if (selection.rangeCount > 0) {
                          selection.removeAllRanges();
                      }
                      selection.addRange(nativeRng);
                  } else {
                      nativeRng.select();
                  }
  
                  return this;
              };
  
              /**
              * Moves the scrollbar to start container(sc) of current range
              *
              * @return {WrappedRange}
              */
              this.scrollIntoView = function (container) {
                  var height = $(container).height();
                  if (container.scrollTop + height < this.sc.offsetTop) {
                      container.scrollTop += Math.abs(container.scrollTop + height - this.sc.offsetTop);
                  }
  
                  return this;
              };
  
              /**
              * @return {WrappedRange}
              */
              this.normalize = function () {
  
                  /**
                  * @param {BoundaryPoint} point
                  * @param {Boolean} isLeftToRight
                  * @return {BoundaryPoint}
                  */
                  var getVisiblePoint = function (point, isLeftToRight) {
                      if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) ||
                      (dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) ||
                      (dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) ||
                      (dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) {
                          return point;
                      }
  
                      // point on block's edge
                      var block = dom.ancestor(point.node, dom.isBlock);
                      if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) ||
                      ((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) {
  
                          // returns point already on visible point
                          if (dom.isVisiblePoint(point)) {
                              return point;
                          }
                          // reverse direction
                          isLeftToRight = !isLeftToRight;
                      }
  
                      var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) :
                      dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint);
                      return nextPoint || point;
                  };
  
                  var endPoint = getVisiblePoint(this.getEndPoint(), false);
                  var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true);
  
                  return new WrappedRange(
                      startPoint.node,
                      startPoint.offset,
                      endPoint.node,
                      endPoint.offset
                  );
              };
  
              /**
              * returns matched nodes on range
              *
              * @param {Function} [pred] - predicate function
              * @param {Object} [options]
              * @param {Boolean} [options.includeAncestor]
              * @param {Boolean} [options.fullyContains]
              * @return {Node[]}
              */
              this.nodes = function (pred, options) {
                  pred = pred || func.ok;
  
                  var includeAncestor = options && options.includeAncestor;
                  var fullyContains = options && options.fullyContains;
  
                  // TODO compare points and sort
                  var startPoint = this.getStartPoint();
                  var endPoint = this.getEndPoint();
  
                  var nodes = [];
                  var leftEdgeNodes = [];
  
                  dom.walkPoint(startPoint, endPoint, function (point) {
                      if (dom.isEditable(point.node)) {
                          return;
                      }
  
                      var node;
                      if (fullyContains) {
                          if (dom.isLeftEdgePoint(point)) {
                              leftEdgeNodes.push(point.node);
                          }
                          if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) {
                              node = point.node;
                          }
                      } else if (includeAncestor) {
                          node = dom.ancestor(point.node, pred);
                      } else {
                          node = point.node;
                      }
  
                      if (node && pred(node)) {
                          nodes.push(node);
                      }
                  }, true);
  
                  return list.unique(nodes);
              };
  
              /**
              * returns commonAncestor of range
              * @return {Element} - commonAncestor
              */
              this.commonAncestor = function () {
                  return dom.commonAncestor(sc, ec);
              };
  
              /**
              * returns expanded range by pred
              *
              * @param {Function} pred - predicate function
              * @return {WrappedRange}
              */
              this.expand = function (pred) {
                  var startAncestor = dom.ancestor(sc, pred);
                  var endAncestor = dom.ancestor(ec, pred);
  
                  if (!startAncestor && !endAncestor) {
                      return new WrappedRange(sc, so, ec, eo);
                  }
  
                  var boundaryPoints = this.getPoints();
  
                  if (startAncestor) {
                      boundaryPoints.sc = startAncestor;
                      boundaryPoints.so = 0;
                  }
  
                  if (endAncestor) {
                      boundaryPoints.ec = endAncestor;
                      boundaryPoints.eo = dom.nodeLength(endAncestor);
                  }
  
                  return new WrappedRange(
                      boundaryPoints.sc,
                      boundaryPoints.so,
                      boundaryPoints.ec,
                      boundaryPoints.eo
                  );
              };
  
              /**
              * @param {Boolean} isCollapseToStart
              * @return {WrappedRange}
              */
              this.collapse = function (isCollapseToStart) {
                  if (isCollapseToStart) {
                      return new WrappedRange(sc, so, sc, so);
                  } else {
                      return new WrappedRange(ec, eo, ec, eo);
                  }
              };
  
              /**
              * splitText on range
              */
              this.splitText = function () {
                  var isSameContainer = sc === ec;
                  var boundaryPoints = this.getPoints();
  
                  if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) {
                      ec.splitText(eo);
                  }
  
                  if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) {
                      boundaryPoints.sc = sc.splitText(so);
                      boundaryPoints.so = 0;
  
                      if (isSameContainer) {
                          boundaryPoints.ec = boundaryPoints.sc;
                          boundaryPoints.eo = eo - so;
                      }
                  }
  
                  return new WrappedRange(
                      boundaryPoints.sc,
                      boundaryPoints.so,
                      boundaryPoints.ec,
                      boundaryPoints.eo
                  );
              };
  
              /**
              * delete contents on range
              * @return {WrappedRange}
              */
              this.deleteContents = function () {
                  if (this.isCollapsed()) {
                      return this;
                  }
  
                  var rng = this.splitText();
                  var nodes = rng.nodes(null, {
                      fullyContains: true
                  });
  
                  // find new cursor point
                  var point = dom.prevPointUntil(rng.getStartPoint(), function (point) {
                      return !list.contains(nodes, point.node);
                  });
  
                  var emptyParents = [];
                  $.each(nodes, function (idx, node) {
                      // find empty parents
                      var parent = node.parentNode;
                      if (point.node !== parent && dom.nodeLength(parent) === 1) {
                          emptyParents.push(parent);
                      }
                      dom.remove(node, false);
                  });
  
                  // remove empty parents
                  $.each(emptyParents, function (idx, node) {
                      dom.remove(node, false);
                  });
  
                  return new WrappedRange(
                      point.node,
                      point.offset,
                      point.node,
                      point.offset
                  ).normalize();
              };
  
              /**
              * makeIsOn: return isOn(pred) function
              */
              var makeIsOn = function (pred) {
                  return function () {
                      var ancestor = dom.ancestor(sc, pred);
                      return !!ancestor && (ancestor === dom.ancestor(ec, pred));
                  };
              };
  
              // isOnEditable: judge whether range is on editable or not
              this.isOnEditable = makeIsOn(dom.isEditable);
              // isOnList: judge whether range is on list node or not
              this.isOnList = makeIsOn(dom.isList);
              // isOnAnchor: judge whether range is on anchor node or not
              this.isOnAnchor = makeIsOn(dom.isAnchor);
              // isOnCell: judge whether range is on cell node or not
              this.isOnCell = makeIsOn(dom.isCell);
              // isOnData: judge whether range is on data node or not
              this.isOnData = makeIsOn(dom.isData);
  
              /**
              * @param {Function} pred
              * @return {Boolean}
              */
              this.isLeftEdgeOf = function (pred) {
                  if (!dom.isLeftEdgePoint(this.getStartPoint())) {
                      return false;
                  }
  
                  var node = dom.ancestor(this.sc, pred);
                  return node && dom.isLeftEdgeOf(this.sc, node);
              };
  
              /**
              * returns whether range was collapsed or not
              */
              this.isCollapsed = function () {
                  return sc === ec && so === eo;
              };
  
              /**
              * wrap inline nodes which children of body with paragraph
              *
              * @return {WrappedRange}
              */
              this.wrapBodyInlineWithPara = function () {
                  if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) {
                      sc.innerHTML = dom.emptyPara;
                      return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0);
                  }
  
                  /**
                  * [workaround] firefox often create range on not visible point. so normalize here.
                  *  - firefox: |<p>text</p>|
                  *  - chrome: <p>|text|</p>
                  */
                  var rng = this.normalize();
                  if (dom.isParaInline(sc) || dom.isPara(sc)) {
                      return rng;
                  }
  
                  // find inline top ancestor
                  var topAncestor;
                  if (dom.isInline(rng.sc)) {
                      var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline));
                      topAncestor = list.last(ancestors);
                      if (!dom.isInline(topAncestor)) {
                          topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so];
                      }
                  } else {
                      topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0];
                  }
  
                  // siblings not in paragraph
                  var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse();
                  inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline));
  
                  // wrap with paragraph
                  if (inlineSiblings.length) {
                      var para = dom.wrap(list.head(inlineSiblings), 'p');
                      dom.appendChildNodes(para, list.tail(inlineSiblings));
                  }
  
                  return this.normalize();
              };
  
              /**
              * insert node at current cursor
              *
              * @param {Node} node
              * @return {Node}
              */
              this.insertNode = function (node) {
                  var rng = this.wrapBodyInlineWithPara().deleteContents();
                  var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node));
  
                  if (info.rightNode) {
                      info.rightNode.parentNode.insertBefore(node, info.rightNode);
                  } else {
                      info.container.appendChild(node);
                  }
  
                  return node;
              };
  
              /**
              * insert html at current cursor
              */
              this.pasteHTML = function (markup) {
                  var contentsContainer = $('<div></div>').html(markup)[0];
                  var childNodes = list.from(contentsContainer.childNodes);
  
                  var rng = this.wrapBodyInlineWithPara().deleteContents();
  
                  return childNodes.reverse().map(function (childNode) {
                      return rng.insertNode(childNode);
                  }).reverse();
              };
  
              /**
              * returns text in range
              *
              * @return {String}
              */
              this.toString = function () {
                  var nativeRng = nativeRange();
                  return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text;
              };
  
              /**
              * returns range for word before cursor
              *
              * @param {Boolean} [findAfter] - find after cursor, default: false
              * @return {WrappedRange}
              */
              this.getWordRange = function (findAfter) {
                  var endPoint = this.getEndPoint();
  
                  if (!dom.isCharPoint(endPoint)) {
                      return this;
                  }
  
                  var startPoint = dom.prevPointUntil(endPoint, function (point) {
                      return !dom.isCharPoint(point);
                  });
  
                  if (findAfter) {
                      endPoint = dom.nextPointUntil(endPoint, function (point) {
                          return !dom.isCharPoint(point);
                      });
                  }
  
                  return new WrappedRange(
                      startPoint.node,
                      startPoint.offset,
                      endPoint.node,
                      endPoint.offset
                  );
              };
  
              /**
              * create offsetPath bookmark
              *
              * @param {Node} editable
              */
              this.bookmark = function (editable) {
                  return {
                      s: {
                          path: dom.makeOffsetPath(editable, sc),
                          offset: so
                      },
                      e: {
                          path: dom.makeOffsetPath(editable, ec),
                          offset: eo
                      }
                  };
              };
  
              /**
              * create offsetPath bookmark base on paragraph
              *
              * @param {Node[]} paras
              */
              this.paraBookmark = function (paras) {
                  return {
                      s: {
                          path: list.tail(dom.makeOffsetPath(list.head(paras), sc)),
                          offset: so
                      },
                      e: {
                          path: list.tail(dom.makeOffsetPath(list.last(paras), ec)),
                          offset: eo
                      }
                  };
              };
  
              /**
              * getClientRects
              * @return {Rect[]}
              */
              this.getClientRects = function () {
                  var nativeRng = nativeRange();
                  return nativeRng.getClientRects();
              };
          }