パスを操作する便利関数を作った

グリモンで遊んでるんだけど、どうもGM_xmlhttpRequestは相対URLを使えないようなので仕方なくパス操作系のライブラリを作ってみた。
いかにもありそうなのにいいものが見つからなかったのは検索スキルが低いからなのか。


GM_xmlhttpRequestに絶対URLで渡すことが目的だったので、

  • http://〜/foo/bar.htmlからhttp://〜/foo/を得る
  • http://〜/foo/と../baz/quux.jpgからhttp://〜/quux.jpgを得る
  • ついでにhttp://〜/foo/////bar/./baz.htmlみたいなのはhttp://〜/foo/bar/baz.htmlに正規化する

ということが実現できるだけの機能を実装。
basenameとdirnameはPHPbasename()dirname()の挙動に合わせた。
canonicalize、collapse、joinはPerlFile::Specを参考にした。

API

使い方は次のような感じ。インスタンス化するようなものではないけど、まとめたかったのでオブジェクトにした。

var dir = Path.dirname('http://example.com/foo/bar.html');
alert(dir); // http://example.com/foo
var abs = Path.join(dir, '../bar/baz.png');
alert(abs); // http://example.com/bar/baz.png
basename(path)
/foo/bar/baz.htmlからbaz.htmlを取得する。PHPの同名関数と同じ。
dirname(path)
/foo/bar/baz.htmlから/foo/barを取得する。PHPの同名関数と同じ。
canonicalize(path)
/foo/////bar/./baz.htmlみたいなやつを/foo/bar/baz.htmlに正規化する。但し../は解決しない。
collapse(path)
/foo/bar/../baz/quux.htmlみたいなやつを/foo/baz/quux.htmlにする。
join(dir, path)
2つのパスを結合する。canonicalizeやcollapseは適当にやってくれる。/foo/barと../baz/quux.htmlを結合すると/foo/baz/quux.htmlになる。


注意点としてはjoinの一つ目はディレクトリを前提としているので、たとえ/foo/bar.htmlなんてパスであったとしても、ディレクトリと見なして後ろにくっつける。都合が悪ければ渡す前にdirnameすべし。
もうひとつ、dirnameが返すパスの末尾にはスラッシュは含まれない("/"のときは除く)。

ソースコード

var Path = {
    // get filename part from path
    basename : function(path) {
        path = path.replace(/\/$/, '');
        if (path === '') return path;

        return path.match(/[^/]+$/);
    },

    // get directory part from path
    dirname : function(path) {
        if (path == '/') return path;
        if (path == '../') return '.';
        if (!path.match(/\//)) path = './' + path;

        var dirname = path.replace(/\/$/, '').replace(/\/[^/]*$/, '');
        if (dirname === '') return '/';

        return dirname;
    },

    // join paths
    join : function(base, path) {
        var host = null;
        if (base.match(/^(\w+:\/\/[^\/]+)(.*)/)) {
            host = RegExp.$1;
            base = RegExp.$2;
        }

        var result = host == null ? '' : host;
        result += Path.collapse(Path.canonicalize(base + '/' + path));

        return result;
    },

    // canonicalize path such as "aaa/./bbb//ccc"
    // but doesn't resolve updir "../"
    canonicalize : function(path) {
        path = path.replace(/\/+/g, '/').replace(/(\/\.)+(\/|$)/g, '/');
        if (path !== './') path = path.replace(/^(\.\/)+/, '');
        path = path.replace(/^\/(\.\.\/)+/, '/').replace(/^\/\.\.$/, '/');
        if (path !== '/') path = path.replace(/\/$/, '');

        return path;
    },

    // resolve "../"
    collapse : function(path) {
        var parts = path.indexOf('/') === 0 ? path.substr(1).split(/\//) : path.split(/\//);
        var collapsed = new Array();
        var over = 0;
        for (var i = 0; i < parts.length; i++) {
            if (parts[i] == '..') {
                if (collapsed.length) collapsed.pop();
                else over++;
            } else {
                collapsed.push(parts[i]);
            }
        }

        if (path.indexOf('/') === 0)
            return '/' + collapsed.join('/');

        while (over-- > 0)
            collapsed.unshift('..');

        return collapsed.join('/');
    }
};


"Path"は大胆だったかもしれない。