New Features in Android Browser 4.0

via http://www.mobilexweb.com/blog/android-4-0-browser-html5

ブックマークの同期機能は iPhone Safari にも欲しい機能です。
手早くブックマークしておいて、あとからタブレットやPCで見るといった使い方ができますね。

Android Browser が SVG をサポート

これでブラウザ側のサポートはひと通り揃ったのですが。ツールや作業者のこなれ具合を鑑みると、

  • フリーのオーサリングツールが普及していない。SVG は HTML や CSS のようにテキストエディタでハンドリングしきれるものではない
  • デザイナーとプログラマーの繋ぎこみノウハウが蓄積していない
  • SVG の互換性情報の共有もまだまだ
  • SVG の仕様が巨大で、ついつい欲張ると物ができない
  • IE が、ここでも足をひっぱる
  • SVG DOM をサポートしているライブラリが無い

といった弱みがあります。これらをクリアしないと SVG ブームは来ないな〜 という感じしてます。

SVG DOM をサポートするライブラリが登場すれば、 SVG で UI を構築する試みが始まり、そのへんから普及が始まるんじゃないかな〜 とも思いますが。
# uupaa.js 0.8 に仕込んでおいた SVG DOM ビルダー機能も、やっと陽の目を見るかな〜 と

mm.mfx 群舞向きアニメーションの実装

uupaa2011-10-14


uupaa.js 0.8 で書いた uu.fx は1つのオブジェクトを高速で滑らかにアニメーションさせる能力に特化してまして、
沢山のオブジェクトが整列してパラパラと切り替わるタイプのアニメーションには使いづらいものでした。

ずっと宿題になってましたが、requestAnimationFrame を利用し、mm.mfxとして実装してみました。
# mfx は Mass Effectの略です

デモ: http://mofmof-js.googlecode.com/svn/trunk/test/Math.easing/tiling.htm

ブラウザ専用にコードを書いてないので、このままだとちょっと滑らかさが足りないのですが、ブラウザに特化した版ものちほど実装する予定です。

(ε・◇・)з CoffeeScript と mofmof.js

(ε・◇・)з CoffeeScriptRubyPython っぽく書けて JavaScriptコンパイルできるプリティな言語だとか!
(ε・◇・)з 噂では CoffeeScript で書くとコードが短くなるとか!!

物は試しに http://jashkenas.github.com/coffee-script/ にある幾つかのコード片と、mofmof.js 混じりで書いた場合のコードを比べてみました。

(ε・◇・)з mofmof.js はここだよ → http://code.google.com/p/mofmof-js/w/list

Syntax
// coffee

    # Assignment:
    number   = 42
    opposite = true

    # Conditions:
    number = -42 if opposite

    # Functions:
    square = (x) -> x * x

    # Arrays:
    list = [1, 2, 3, 4, 5]

    # Objects:
    math =
      root:   Math.sqrt
      square: square
      cube:   (x) -> x * square x

    # Splats:
    race = (winner, runners...) ->
      print winner, runners

    # Existence:
    alert "I knew it!" if elvis?

    # Array comprehensions:
    cubes = (math.cube num for num in list)

// coffee -> js ----------------------------

    var cubes, list, math, num, number, opposite, race, square;
    var __slice = Array.prototype.slice;
    number = 42;
    opposite = true;
    if (opposite) number = -42;
    square = function(x) {
      return x * x;
    };
    list = [1, 2, 3, 4, 5];
    math = {
      root: Math.sqrt,
      square: square,
      cube: function(x) {
        return x * square(x);
      }
    };
    race = function() {
      var runners, winner;
      winner = arguments[0], runners = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
      return print(winner, runners);
    };
    if (typeof elvis !== "undefined" && elvis !== null) alert("I knew it!");
    cubes = (function() {
      var _i, _len, _results;
      _results = [];
      for (_i = 0, _len = list.length; _i < _len; _i++) {
        num = list[_i];
        _results.push(math.cube(num));
      }
      return _results;
    })();

// mofmof ----------------------------

    // Assignment:
    var number = 42, opposite = true;

    // Conditions:
    opposite && (number = -42);

    // Functions:
    function square(x) { return x * x; }

    // Arrays:
    list = [1, 2, 3, 4, 5];

    // Objects:
    math = {
        root:   Math.sqrt,
        square: square,
        cube:   function(x) { return x * square(x); }
    };

    // Splats:
    function cube(winner, runners, /*... */) {
        return print.apply(null, arguments);
    }

    // Existence:
    elvis != null && alert("I knew it!");

    // Array comprehensions:
    cubes = list.filter(math.cube);

(ε・◇・)з 基本 Syntax だと、あまり変わった感じしませんね〜

Functions
// coffee

    square = (x) -> x * x
    cube   = (x) -> square(x) * x

    fill = (container, liquid = "coffee") ->
      "Filling the #{container} with #{liquid}..."

// coffee -> js ----------------------------

    var cube, square;
    square = function(x) {
      return x * x;
    };
    cube = function(x) {
      return square(x) * x;
    };
    var fill;
    fill = function(container, liquid) {
      if (liquid == null) liquid = "coffee";
      return "Filling the " + container + " with " + liquid + "...";
    };

// mofmof.js ----------------------------

    function square(x) { return x * x; };
    function cube(x)   { return square(x) * x; }

    function fill(container, liquid /* = coffee */) {
        return "Filling the @@ with @@...".f(container, liquid || "coffee");
    }

(ε・◇・)з Coffee は function が -> になってる分だけタイプ数少なめですね

Loops and Comprehensions
// coffee
    # Eat lunch.
    eat food for food in ['toast', 'cheese', 'wine']

// coffee -> js ----------------------------

    var food, _i, _len, _ref;
    _ref = ['toast', 'cheese', 'wine'];
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
      food = _ref[_i];
      eat(food);
    }

// mofmof ----------------------------

    ['toast', 'cheese', 'wine'].forEach(eat);

            or

    var iter = Hash(['toast', 'cheese', 'wine']), food;
    while (food = iter.next()) {
        eat(food);
    }

(ε・◇・)з mofmof.js はforEachやイテレータ使ってます。実行速度が重要でなければ、このへんは好みかな〜

// coffee

    countdown = (num for num in [10..1])

// coffee -> js ----------------------------

    var countdown, num;
    countdown = (function() {
      var _results;
      _results = [];
      for (num = 10; num >= 1; num--) {
        _results.push(num);
      }
      return _results;
    })();

// mofmof ----------------------------

    countdown = 10..$(1);

(ε・◇・)з mofmof.js は Number.prototype.$ を拡張して 10..$(1) で 10 から1までの配列を生成していますよ〜

// coffee

    yearsOld = max: 10, ida: 9, tim: 11

    ages = for child, age of yearsOld
      "#{child} is #{age}"

// coffee -> js ----------------------------

    var age, ages, child, yearsOld;
    yearsOld = {
      max: 10,
      ida: 9,
      tim: 11
    };
    ages = (function() {
      var _results;
      _results = [];
      for (child in yearsOld) {
        age = yearsOld[child];
        _results.push("" + child + " is " + age);
      }
      return _results;
    })();

// mofmof ----------------------------
    var yearsOld = { max: 10, ida: 9, tim: 11 };
    Hash.map(yearsOld, function(age, child) {
        return "@@ is @@".f(child, age);
    });

(ε・◇・)з Coffee側のコードはシンプルですね〜

Array Slicing and Splicing with Ranges
// coffee
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    copy    = numbers[0...numbers.length]

    middle  = copy[3..6]

// coffee -> js ----------------------------
    var copy, middle, numbers;
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    copy = numbers.slice(0, numbers.length);
    middle = copy.slice(3, 7);

// mofmof ----------------------------
    var numbers = 0..$(9);

    var copy    = numbers.clip(0, numbers.length);

    var middle  = copy.clip(3, 6);

(ε・◇・)з もふもふ!

// coffee
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    numbers[3..6] = [-3, -4, -5, -6]

// coffee -> js ----------------------------
    var numbers, _ref;
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    [].splice.apply(numbers, [3, 4].concat(_ref = [-3, -4, -5, -6])), _ref;

// mofmof ----------------------------
    numbers = 0..$(9);
    numbers.swap(3, (-3).$(-6));

(ε・◇・)з mofmof 側は、Array#swap で配列の一部をごっそり入れ替えてます〜

Everything is an Expression (at least, as much as possible)
// coffee
    globals = (name for name of window)[0...10]

// coffee -> js ----------------------------
    var globals, name;
    globals = ((function() {
      var _results;
      _results = [];
      for (name in window) {
        _results.push(name);
      }
      return _results;
    })()).slice(0, 10); // 全部とってきて10個だけ返す

// mofmof ----------------------------
    globals = Hash.keys(window, 10); // 10個とってきて返す

(ε・◇・)з mofmof の Hash.keys は、第二引数で列挙する要素の最大数を指定できるので、10個だけ列挙して返します〜

Classes, Inheritance, and Super
// coffee

    class Animal
      constructor: (@name) ->

      move: (meters) ->
        alert @name + " moved #{meters}m."

    class Snake extends Animal
      move: ->
        alert "Slithering..."
        super 5

    class Horse extends Animal
      move: ->
        alert "Galloping..."
        super 45

    sam = new Snake "Sammy the Python"
    tom = new Horse "Tommy the Palomino"

    sam.move()
    tom.move()


// coffee -> js ----------------------------
    var Animal, Horse, Snake, sam, tom;
    var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
      for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
      function ctor() { this.constructor = child; }
      ctor.prototype = parent.prototype;
      child.prototype = new ctor;
      child.__super__ = parent.prototype;
      return child;
    };
    Animal = (function() {
      function Animal(name) {
        this.name = name;
      }
      Animal.prototype.move = function(meters) {
        return alert(this.name + (" moved " + meters + "m."));
      };
      return Animal;
    })();
    Snake = (function() {
      __extends(Snake, Animal);
      function Snake() {
        Snake.__super__.constructor.apply(this, arguments);
      }
      Snake.prototype.move = function() {
        alert("Slithering...");
        return Snake.__super__.move.call(this, 5);
      };
      return Snake;
    })();
    Horse = (function() {
      __extends(Horse, Animal);
      function Horse() {
        Horse.__super__.constructor.apply(this, arguments);
      }
      Horse.prototype.move = function() {
        alert("Galloping...");
        return Horse.__super__.move.call(this, 45);
      };
      return Horse;
    })();
    sam = new Snake("Sammy the Python");
    tom = new Horse("Tommy the Palomino");
    sam.move();
    tom.move();

// mofmof ----------------------------
    mm.Class("Animal", {
        init: function(name) {
            this.name = name;
        },
        move: function(meters) {
            alert("@@ moved @@m.".f(this.name, meters));
        }
    });
    mm.Class("Snake:Animal", {
        move: function() {
            alert("Slithering...");
            this.superCall("move", 5);
        }
    });
    mm.Class("Horse:Animal", {
        move: function() {
            alert("Galloping...");
            this.superCall("move", 45);
        }
    });
    var sam = mm("Snake", "Sammy the Python");
    var tom = mm("Horse", "Tommy the Palomino");

    sam.move();
    tom.move();

(ε・◇・)з mofmof にもクラスあるんだよ〜
(ε・◇・)з 最大継承数は1段に限定してある(昔は3段までサポートしてた)けど、1段継承できればニーズの9割カバーできるからOKなんだよ〜 (それ以上継承したい人は変態的なコードを書きたい人だけだと思うよ〜

Destructuring Assignment
// coffee
    theBait   = 1000
    theSwitch = 0

    [theBait, theSwitch] = [theSwitch, theBait]

// coffee -> js ----------------------------
    var theBait, theSwitch, _ref;
    theBait = 1000;
    theSwitch = 0;
    _ref = [theSwitch, theBait], theBait = _ref[0], theSwitch = _ref[1];

// mofmof ----------------------------
    var theBait = 1000, theSwitch = 0;

    [theBait, theSwitch].swap(0, [theSwitch, theBait]);

(ε・◇・)з ここでも Array#swap が出てきてます〜

Chained Comparisons
// coffee
    cholesterol = 127

    healthy = 200 > cholesterol > 60

// coffee -> js ----------------------------
    var cholesterol, healthy;
    cholesterol = 127;
    healthy = (200 > cholesterol && cholesterol > 60);

// mofmof ----------------------------
    var cholesterol = 127

    healthy = cholesterol.inRange(200, 60);

(ε・◇・)з Coffee のほうが分かりやすいね〜

おしまい

(ε・◇・)з mofmof.js も結構イケてるんじゃないでしょうか!

Screen Transition Traversal Pattern

とある開発環境で iPhone/Android アプリを書いてます。開発言語は js ですが Titanium ではありません。

今回の開発で実現したい事の1つに「可能なら UI を自動でテストしたい」というのがあります。
ぼーっとしていたら、以下のようなロジック(パターン)を ピコーン しました (恐らくボクが知らないだけで車輪の再発明なんだろうけどさ!)

  • 画面をディレクトリと見立てる。ディレクトリ=画面。ファイルに該当するものは無し
    • / はルートディレクトリ(ルート画面)。常に存在する
    • /A はルート直下のAディレクトリ(画面A)。親画面の上に表示される子画面達は /A/B/C のように表現する
  • 画面の状態をディレクトリパスにパラメタとして与えるだけで、ネストした画面を再現できる
    • /A(key1=value2;key2=value)/B/C といったパスは、画面Aを構築する際に key1=value2;key2=value といったパラメタが与えられる
    • テスト環境から cd /A(key1=value2;key2=value)/B/C と入力すると、画面Cが一番上に表示された状態が再現できる

一言でいうと、画面構造をREST風味にステートレス化するということです。途中の画面の状態を再現するパラメタをパスに埋め込む事ができ、再現する仕組みです。

このような構造を取ることにより、

  • 画面A が 画面B を知らず、画面B が 画面A を知らなくても成立する(かもしれない)
    • 開発終盤の仕様変更と再テストに強くなる。途中にどのような画面が新たに挟まっても手直しが少ない(かもしれない)
  • 画面A から 画面B に遷移する UI (ボタン等)には、最終的に /A/B といったパスが設定されるが、このパスは画面A が固定で持たず、UI 全体を統括するマネージャ的存在が 画面A に知らせる。

といった良いことがありそうです。

こんな感じ?

たたき台を考えてみました。mofmof.js ベースです。

閉じる/開く画面を決定するロジックのキモは、 mm.Class.Screen#resolve に実装する形になります(まだ空っぽです)

mm.Class.singleton("Screen", { // シングルトンクラスの定義。 mm("Screen").pwd() のように使用する
  _current: "/",               // カレントパス
  pwd: function() { // @return String: カレントパスを返す。pwd で通じるよね?
      return this._current;
  },
  cd: function(path) { // @param String: 移動先のパスを指定する
      var r = this.resolve(this._current, path); // 閉じる/開く画面の解決
      
      if (r.close.length) {
          // r.close の順に画面を破棄する
      }
      if (r.open.length) {
          // r.open の順に画面を破棄する
      }
      this._current = path; // カレントパスを更新
  },
  resolve: function(before,  // @param String:
                    after) { // @param String:
                             // @return Hash: { close, open, base }
                             //   close - StringArray:
                             //   open - StringArray:
                             //   base - String: base dir to open
    if (before === after) {
        return { close: [], open: [], base: "" };
    }

    var rv = { close: [], open: [], before: base: "" }, ...

    // closeする画面と、openする画面の配列 をここで作成する

    return rv;    
  }
});

予想では、resolve メソッドの結果は以下のようになるはずです。

mm("Screen").resolve("/A/B/C", "/A/B/C") -> { close: [], open: [], before: "/A/B/C", after: "/A/B/C" }
mm("Screen").resolve("/A/B/C", "/A/B") -> { close: ["C"], open: [], before: "/A/B/C", after: "/A/B" }
mm("Screen").resolve("/A/B/C", "/A/D") -> { close: ["C", "B"], open: ["D"], before: "/A/B/C", after: "/A/D" }
mm("Screen").resolve("/A/B/C", "/") -> { close: ["C", "B", "A"], open: [], before: "/A/B/C", after: "/" }
mm("Screen").resolve("/A/B/C", "/E/F") -> { close: ["C", "B", "A"], open: ["E", "F"], before: "/A/B/C", after: "/E/F" }

現在実装中なのですが、resolve が中々に手強いです。

実装できたらこのエントリを更新します。

追記

散々悩んで書いた汚いロジックがこちら。
「画面遷移どうすべ〜」→ ピコーン待ち → ピコーンきたー → ブログカキカキ → コードカキカキ 含めて8時間ぐらい。

    resolve: function(before,  // @param String:
                      after) { // @param String:
                               // @return Hash: { close, open, base }
                               //   close - StringArray:
                               //   open - StringArray:
                               //   base - String:
        if (before === after) { // match all -> nop
            return { close: [], open: [], base: "" };
        }

        var close = [], open = [], base = "/", bary, aary, last, i, iz;

        bary = before.split("/"); // "/A/B/C" -> ["", A, B, C]
        aary = after.split("/");  // "/A/B"   -> ["", A, B]

        // close
        while ((last = bary.pop())) {
            close.push(last);
            aary.length = bary.length;
            if (bary.join("/") === aary.join("/")) { // match
                break;
            }
        }
        bary = before.split("/"); // "/A/B/C" -> ["", A, B, C]
        aary = after.split("/");  // "/A/B"   -> ["", A, B]

        if (after === "/") { // case: resolve("/A/B/C", "/")
            ;
        } else if (bary[1] !== aary[1]) { // case: resolve("/A/B/C", "/E/F")
            // ルート直下から違う場合は全てopen対象となる
            base = "/";
            open = aary;
            open.shift();
        } else {
            // open ,, close とは逆方向に走査を行い不一致要素を open に追加する
            //      ,, 画面を開く際の起点となる要素をopenParentに設定する
            for (i = 1, iz = Math.max(bary.length, aary.length); i < iz; ++i) {
                if (aary[i] === void 0) { // outof index
                    break;
                }
                if (base) {
                    // 親画面が異なるため、親画面以下の全ての画面を追加する
                    if (bary[i] !== aary[i]) {
                        for (iz = aary.length; i < iz; ++i) {
                            open.push(aary[i]);
                            console.log("add @@".f(aary[i]));
                        }
                        break;
                    }
                } else {
                    base = bary[i];
                }
            }
        }
        return { close: close, open: open, base };
    }

(↑)のロジックは見て分かるように、最悪のコードです。日本語コメントが必要なくらいに最悪です。自分で書いてて「これはメンテできないわー」ってなりました。さらに幾つかのテストケースをパスできず「あかん、これはあかん…」とクソ悩んでました。
困ってしまって、2つ隣に座ってる同僚(セト神様)をティディベアに見立て、一人ティディベアデバッグ(夜中の4:00に一方的にブツブツ話かけたった)をした所、再度ピコーンが!!

セト神の啓示をうけ、5分でリライトしたロジックがこちら

    resolve: function(before,  // @param String:
                      after) { // @param String:
                               // @return Hash: { close, open, base }
                               //   close - StringArray:
                               //   open - StringArray:
                               //   base - String: base dir to open
        var close = [], open = [], ary1, ary2, base = "", i = 0, j = 0;

        if (before !== after) {
            ary1 = before.split("/"); // "/A/B/C" -> ["", A, B, C]
            ary2 = after.split("/");  // "/A/B"   -> ["", A, B]

            while (ary1[i] === ary2[i]) {
                base = ary1[i++] || "/";
            }
            j = i;
            while (ary1[i]) {
                close.push(ary1[i++]);
            }
            while (ary2[j]) {
                open.push(ary2[j++]);
            }
        }
        return { close: close.reverse(), open: open, base: base };
    }

テストコード

(function() {
'mm("Screen").resolve("/", "/")'.test('{ close: [], open: [], base: "" }');
'mm("Screen").resolve("/A", "/Z")'.test('{ close: ["A"], open: ["Z"], base: "/" }');
'mm("Screen").resolve("/", "/A/B/C")'.test('{ close: [], open: ["A","B","C"], base: "/" }');
'mm("Screen").resolve("/A", "/A/B/C")'.test('{ close: [], open: ["B","C"], base: "A" }');
'mm("Screen").resolve("/A/B/C", "/A/B/C")'.test('{ close: [], open: [], base: "" }');
'mm("Screen").resolve("/A/B/C", "/A/B")'.test(  '{ close: ["C"], open: [], base: "B" }');
'mm("Screen").resolve("/A/B/C", "/A/D")'.test(  '{ close: ["C", "B"], open: ["D"], base: "A" }');
'mm("Screen").resolve("/A/B/C", "/")'.test(     '{ close: ["C", "B", "A"], open: [], base: "/" }');
'mm("Screen").resolve("/A/B/C", "/E/F")'.test(  '{ close: ["C", "B", "A"], open: ["E", "F"], base: "/" }');
'mm("Screen").resolve("/A/B/C/D/E", "/A/Z/C/D/E")'.test('{ close: ["E", "D", "C", "B"], open: ["Z", "C", "D", "E"], base: "A" }');

"run".test("core.js - mm.Class.Screen");
})();

何が言いたいかというと、ティディベアデバッグ☆オススメ☆デス!!!!

IE10 で attachEvent が廃止された場合に備えましょう

Windows 8 には IE10 が標準搭載されます。これらは早ければ2012年の秋ごろまでにリリースされる予定です。
IE10 ではレガシーな(古い)機能やメソッドが切り捨てられると予告されています → 非推奨の DOM イベント

IE10 で attachEvent が廃止されると attachEventで IE かどうかを判別する(↓)のコードなどが魔女化するでしょう(バグになるでしょう)。

// オレオレライブラリによく見かけるコード片
var isIE = (window.attachEvent && !window.opera);

        この場合は ↓↓ これで良さげ

var isIE = !!document.uniqueID;

アドネットワークからの広告を埋め込んでいるサイトでは、同様のコードが広告生成用の JavaScript の中にも埋まっているかもしれません。そちらに対する備えも必要ですね。

NO TEST, NO LIFE. NO DOC, NO LIFE

uupaa.js や mofmof.js には {@hoge 〜 }@hoge のようなコードブロックを切り落として Minify する機能があるので、「ソースコードにテストもドキュメントも全部埋め込むことが可能だな〜」って3年程前から考えてました。

そこで、Function.prototype.spec というメソッドを追加し、これにスペックを書き貯めたらどうだろうか(?)とか考えました。

たとえば

Array.range(1,7) で [1..7] 的な連続した数値の配列を生成する Array.range 関数があったとすると

// Array.range - range generator
function Array_range(begin,    // @param Number: begin
                     end,      // @param Number: end
                     filter) { // @param Function/Number(= 1): filter or skip count
                               // @return Array: [Number, ...]
                               // @raise: Error("BAD_ARG")
                               // @see: Array#range
    var rv = [], ri = 0, i = begin, iz = end, skip = 1;

    if (Type.isFunction(filter)) {
        for (; i <= iz; ++i) {
            if (filter(i) === true) {
                rv[ri++] = i;
            }
        }
        return rv;
    }
    if (Type.isNumber(filter)) {
        skip = filter;
    }
    if (skip <= 0) {
        throw new Error("BAD_ARG");
    }
    for (; i <= iz; i += skip) {
        rv[ri++] = i;
    }
    return rv;
}

この場合、スペックは以下のような感じで記述してます。

//{@spec
Array.range.spec({
    desc:   ["Function",            "range generator"],
    begin:  ["Number",              "begin"],
    end:    ["Number",              "end"],
    filter: ["Function/Number(= 1)","filter or skip count"],
    ret:    ["Array",               "[Number, ...]"],
    raise:  'Error("BAD_ARG")',
    see:    "Array#range"
});
//}@spec

まだ荒削りだけど、

  1. Function.prototype.spec._spec にスペック一覧が保存されている
  2. Function.prototype.spec._src に関数を実行可能な形(改行やコメントも含んだ文字列)で格納している
  3. node.js 上で走らせて、html + css に落とし込めば、ドキュメントが生成できる
  4. 関数がソースコードに書かれた文字列そのままの形で取り出せるから、ドキュメント上で関数を実行するインタプリタも付けられる
  5. spec に基づき IN と OUT をチェックする assert を自動生成することも可能

とか考えてました。

jsdoc だと、かゆいところに手が届かないので、こういうことになってしまうわけですが。まぁこれはこれで

Function.prototype.spec の実装はこんな感じ

// Function.prototype.spec - add function spec
function Function_spec(hash) { // reserved:{ desc, ret, see, test }
    Function_spec._spec || (Function_spec._spec = {},
                            Function_spec._src  = {});

    var fnName = this.name, // function name
        i, iz, ary;

    // pick up function.name [IE6][IE7][IE8][IE9][Opera9][Opera10.1x]
    if (!fnName) {
        fnName = this + "";
        fnName = fnName.slice(9, fnName.indexOf("(")); // )
    }
    Function_spec._spec[fnName] = hash;
    Function_spec._src[fnName] = this + "";

    if ("test" in hash) {
        ary = hash.test;
        for (i = 0, iz = ary.length; i < iz; i += 3) {
            ary[i].test(ary[i + 1], ary[i + 2] || "");
        }
    }
}

通常利用時は {@spec 〜 }@spec コードブロックを削除して minify する感じです。

//{@spec
Function.prototype.spec     || (Function.prototype.spec     = Function_spec);
String.prototype.test       || (String.prototype.test       = String_test);
//}@spec

String.prototype.test() は類似検索と深度探索を行う、テスト君です。
http://mofmof-js.googlecode.com/svn/trunk/test/object.js.htm をブラウザで表示し、ブラウザのコンソールを見てみると、(↓)のようなセルフテストの結果が出力されます。これをやってるのが、String#test() です。

これも1つのワンソース・マルチユース。テストもドキュメントもコードに埋め込んでしまえば良いのではないのでしょうか?