似たようなリンクをまとめて開くプラグインを作ってみた

リスト化されたリンクをまとめて開きたいとか、文書の目次から同一階層へのリンクをまとめて開きたいとか、画像まとめページから各画像をいっぺんに開きたいことってよくありますよね。

そういうリンクはHTML的に同じ構造になっているという仮定の元、ヒントモードで指定したリンクと同じ構造のリンクを探して一度に開くというプラグインを書いてみました。ソースコードは記事の末尾にあります。

仕組み

指定された要素をEとした時、E.parentNodeをコンテキストノードとしてXPath式"E.tagName"を評価します。結果がE自身のみであった場合、E.parentNode.parentNodeをコンテキストノードとして、XPath式"E.parentNode.tagName"+"/"+"E.tagName"を評価…というの繰り返し、結果にE以外の要素が含まれていたらそこで終わり、その各要素のリンク先をまとめて開きます。単純な仕組みですが、案外うまく動作しています。

<ul>
  <li><a>AAA</a></li>
  <li><a>BBB</a></li>
  <li><a>CCC</a></li>
</ul>

上の例では3つのリンクのうちどれを選択してもAAA, BBB, CCCの3つを開きます。

<div>
  <h3><a>Title A</a></h3>
  <p>...</p>
</div>
<div>
  <h3><a>Title B</a></h3>
  <p>...</p>
</div>

上の例ではどちらのリンクを選択しても、Title A, Bの両方を開けます。

<ul>
  <li><a>AAA</a></li>
  <li><a>BBB</a></li>
</ul>
<ul>
  <li><a>CCC</a></li>
</ul>

現状の実装では上の例で3つとも開きたかったとしても、AAAを選択した場合AAAとBBBが見つかった時点で終了してしまうのでCCCを含めて開くことはできません。ですが、選択したのがCCCだったら3つが開くことになります。対象ページのHTML構造を知らない限りは意識して選択することはできませんが。

こんな実装ですが、例えばGoogleの検索結果ページで1つの見出しを選択すると全ての検索結果をまとめて開くことができます。

ソース

// Vimperator plugin: Open Similar Links

/* Options
 *   g:open_similar_links_map (Default: "S")
 *      Key mapping for open similar links hint mode
 *   g:open_similar_links_hinttags (Default: "//a[@href]")
 *      XPath expression for open similar links
 *   g:open_similar_links_open_limit (Default: 20)
 *      If detect number of tabs over this value, confirm before opening links
 *   g:open_similar_links_open_mode (Default: 1)
 *      1: open links by native window.open
 *      2: open links by liberator.open(URLS, liberator.NEW_BACKGROUND_TAB)
 */

(function(){
    var open_similar_links_map = liberator.globalVariables.open_similar_links_map || "S";
    var open_similar_links_hinttags = liberator.globalVariables.open_similar_links_hinttags || "//a[@href]";
    var open_similar_links_open_limit = liberator.globalVariables.open_similar_links_open_limit || 20;
    var open_similar_links_open_mode = liberator.globalVariables.open_similar_links_open_mode || 1;

    var doc = null;

    function evalXPath(expr, context) {
        return doc.evaluate(expr, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    }

    function openLinks(res) {
        if (!res.snapshotLength) return;

        var urls = [];
        for (var i = 0; i < res.snapshotLength; i++) {
            var elem = res.snapshotItem(i);
            if (elem.href && elem.href.length >= 1) {
                urls.push(elem.href);
            }
        }
        switch (open_similar_links_open_mode) {
        case 2:
            liberator.open(urls.join(", "), liberator.NEW_BACKGROUND_TAB);
            break;
        case 1:
        default:
            var win = content.window;
            var tab_index = tabs.index();
            urls.forEach(function(url) {
                win.open(url);
            });
            tabs.select(tab_index);
            break;
        }
    }

    function search(elem, prev_expr, count) {
        var expr = elem.tagName + prev_expr;
        var res = evalXPath(expr, elem.parentNode);

        if (res.snapshotLength <= 1) {
            if (doc.body != elem.parentNode) {
                search(elem.parentNode, "/" + expr, ++count);
            } else {
                liberator.echo("Not found similar links");
                openLinks(res);
            }
        } else {
            if (res.snapshotLength >= open_similar_links_open_limit) {
                commandline.input("Found " + res.snapshotLength + " links, really open? [y/N]",
                    function (ans) {
                        if (ans.toLowerCase().indexOf("y") == 0) openLinks(res);
                    }, null
                );
            } else {
                liberator.echo("Found " + res.snapshotLength + " links");
                openLinks(res);
            }
        }
    }

    function startSearch(elem) {
        doc = elem.ownerDocument;
        search(elem, "", 0);
    }

    hints.addMode(open_similar_links_map, "Open Similar Links",
        function(elem) {
            startSearch(elem);
        },
        function() { return open_similar_links_hinttags;}
    );
})();