クロスブラウザでRangeオブジェクト扱う2

選択範囲を任意の要素で囲う関数がネック。
Operaは、要素またいだsurroundContentsエラーになる。
テキストノードだけ抜き出して、それぞれ囲む必要がある。
もう少し汎用的になったら、どっかに上げる予定。

/**
 * exRange JavaScript Library v0.3
 * どのブラウザでも簡単にRangeオブジェクトを扱えるライブラリを目指してます。
 * ブログで更新状況公開中
 * http://d.hatena.ne.jp/TakiTake
 *
 * Author: TakiTake <TakiTake.create@gmail.com>
 * Under the NYSL licenses.
 * http://www.kmonos.net/nysl/
 */
(function() {
  window.exRange = function(selection) {
  var
    // 共通して使うメソッド
    common = {
      _from: 0, // 先頭からの選択開始位置
      _to: 0,   // 先頭からの選択終了位置

      /**
       * 制御文字を除いた文字列の長さを返す
       * <s>String.replaceで改行コードが削除できないので、一文字ずつ見て文字列の長さを数える</s>
       * 上の文は、間違い。RegExpの第二引数に"g"オプション渡してなかったorz 修正中
       * @param str 長さを数えたい文字列
       * @return 文字列の長さ
       */
      count: function(str) {
        var
          len = 0,
          unnecessity = '[' + String.fromCharCode(0) + '-' + String.fromCharCode(31) + ']', // ASCIIコードの0-31は制御文字なので排除
          pattern = new RegExp(unnecessity);

        for (var i = 0, l = str.length; i < l; i++) {
          if (!pattern.test(str.charAt(i)))
            len++;
        }

        return len;
      },

      /**
       * 先頭から選択開始位置までの情報を(取得|設定)
       */
      from: function() {
        switch (arguments.length) {
          case 0:
            return this._from;
          case 1:
            this.setFrom(arguments[0]);
        }

        return this;
      },

      /**
       * 先頭から選択終了位置までの情報を(取得|設定)
       */
      to: function() {
        switch (arguments.length) {
          case 0:
            return this._to;
          case 1:
            this.setTo(arguments[0]);
        }

        return this;
      },

      /**
       * 先頭からの選択位置情報を取得
       * @return [選択開始位置, 選択終了位置]
       */
       pos: function() {
         return [this._from, this._to];
       },

      /**
       * 選択開始ノード、位置を(取得|設定)
       */
      start: function() {
        switch (arguments.length) {
          case 0:
            return [this.startContainer, this.startOffset];
          case 1:
          case 2:
            this._setStart.apply(this, arguments);
        }

        return this;
      },

      /**
       * 選択終了ノード、位置を(取得|設定)
       */
      end: function() {
        switch (arguments.length) {
          case 0:
            return [this.endContainer, this.endOffset];
          case 1:
          case 2:
            this._setEnd.apply(this, arguments);
        }

        return this;
      }
    },

    // DOM Level 2 Rangeをサポートしているかどうかでラッパーを変える
    exclusive = (function() {
      var m;

      // DOM Level 2 Rangeをサポートしているブラウザ向けメソッド
      if (document.createRange) {
        m = {
          _setStart: function(node, offset) {
            // 作業用Range作成
            var range = document.createRange();

            // テキストノードじゃないとsetStartできないのでテキストノード出るまで子ノード見てく
            // 改善する必要あり
            while (node.nodeType == 1) {
              node = node.firstChild;
            }

            this.setStart(node, offset || 0);

            // body要素を選択し、選択終了位置を_setStartの引数で再設定する
            // 文字数を数えると先頭からの位置が割り出せるという寸法
            range.selectNode(document.body);
            range.setEnd(node, offset || 0);
            this._from = this.count(range.toString());
          },

          _setEnd: function(node, offset) {
            var range = document.createRange();

            while (node.nodeType == 1) {
              node = node.firstChild;
            }

            this.setEnd(node, offset || 0);

            range.selectNode(document.body);
            range.setEnd(node, offset || 0);
            this._to = this.count(range.toString());
          },

          /**
           * 引数で渡された要素を選択する
           * @param node html要素
           * @return 拡張Rangeオブジェクト
           */
           sel: function(node) {
             this.selectNode(node);
             return this;
           },

          /**
           * 選択範囲内のテキストを引数で指定した要素で囲む
           * @param elem テキストを囲むための要素
           * @return 拡張Rangeオブジェクト
           */
           wrap: function(elem) {
             if (this.startContainer == this.endContainer)
               this.surroundContents(elem);
             else {
               // ここがキモ。作成中
             }
             return this; 
           }  
        }
      // IE用
      } else if (document.body.createTextRange) {
        m = {
          collapsed: false, // Rangeのstartとendの位置が同じかどうか真偽値を返す
          startContainer: null,
          startOffset: 0,
          endContainer: null,
          endOffset: 0,

          setCollapsed: function() {
            if ( (this.startContainer && this.endContainer) && (this.startContainer == this.endContainer) && (this.startOffset == this.endOffset) )
              this.collapsed = true;
            else
              this.collapsed = false;
          },

          _setStart: function(node, offset) {
            // 作業用Range作成
            var range = document.body.createTextRange();

            // 選択終了位置が未設定の場合は設定する
            if (this.endContainer == null) {
              this.moveToElementText(node);
              this.endContainer = node;
              this.endOffset = node.innerText.length;
            }

            // 選択開始ノードと位置を保持
            this.startContainer = node;
            this.startOffset = offset || 0;

            // 選択開始ノードと位置を実際に設定
            range.moveToElementText(node);
            range.moveStart('character', this.startOffset);
            this.setEndPoint('StartToStart', range);

            // ページ先頭からの位置を取得
            range.setEndPoint('EndToStart', range);
            range.setEndPoint('StartToStart', document.body.createTextRange());
            this._from = this.count(range.text);

            this.setCollapsed();
          },

          _setEnd: function(node, offset) {
            // 作業用Range作成
            var range = document.body.createTextRange();

            // 選択開始位置が未設定の場合は設定する
            if (this.startContainer == null) {
              this.moveToElementText(node);
              this.startContainer = node;
              this.startOffset = 0;
            }

            // 選択終了ノードと位置を保持
            this.endContainer = node;
            this.endOffset = offset || 0;

            // 選択終了ノードと位置を実際に設定
            range.moveToElementText(node);
            range.moveStart('character', this.endOffset);
            this.setEndPoint('EndToStart', range);

            // ページ先頭からの位置を取得
            range.setEndPoint('EndToStart', range);
            range.setEndPoint('StartToStart', document.body.createTextRange());
            this._to = this.count(range.text);

            this.setCollapsed();
          },

          sel: function(node) {
            this.moveToElementText(node);

            this.startContainer = node;
            this.startOffset = 0;
            this.endContainer = node;
            this.endOffset = node.innerText.length;

            this.setCollapsed();

            return this;
          },

          wrap: function(elem) {
            // ここもキモ
            elem.innerHTML = this.text;
            this.pasteHTML(elem.outerHTML);
            return this; 
          },

          detach: function() {
            this.setEndPoint('EndToStart', this);
          }
        };
      } else {
        alert('Rangeをサポートしていないブラウザです。\nFirefox, Operaなどのブラウザで閲覧してください。');
        throw 'Can not use exRange.js';
      }

      return m;
    })(),

    // Rangeオブジェクト取得
    range = (function() {
      var r = new Object();

      if (document.createRange)
        r = (selection) ? selection.getRangeAt(0) : document.createRange();
      else if (document.body.createTextRange)
        r = (selection) ? selection.createRange() : document.body.createTextRange(); 

      return r;
    })();

    // 共通メソッドを設定
    for (var o in common)
      range[o] = common[o];

    // ブラウザ別ラッパーメソッドを設定
    for (var o in exclusive)
      range[o] = exclusive[o];

    return range;
};
})();