2014年8月8日金曜日

Vimperator プラグイン direct_bookmark.js で Delicious への Post に失敗する問題に挑む (WIP)

Delicious の API の仕様が替わったため、vimperator プラグイン direct_bookmark.js でマルチバイト文字を <title> に含むページをブックマークできなくなっていた。
不便なので直そうと思っているのだが、とりあえず途中経過を晒しておく。
やったこと
Potential fix for Delicious addLink error by mmerriam · Pull Request #55 · zmanring/chrome-ext-delicious · GitHub で同じような議論がされていて、マルチバイト文字を二重にURLエンコードすればよいと結論付けられている。そこで、Firefox 用拡張 Delicious Post から少しコードを拝借した。
やってることは、 title を1文字ずつバラして、非 ASCII 文字(文字コード>127)だったらエンコードするというもの。
(元の "Delicious Post"では、Delicious 側でうまくデコードできない特殊文字を置換する、という処理もしているが、それは一旦省いた。)
ソースは当記事の最後に載せるとして、まずは動作検証。

1. 当ブログをブクマしてみる
Fig.1 当ブログをDeliciousにPost

Fig.2 正常にPost完了

ブクマできた。よしよし。

2. 別のページをブクマしてみる
私の中では最近ラムネが激アツなので、ラムネに関するページをブクマしてみる。
Fig.3 他所様のページを勝手に使って申し訳ないが、ラムネに関する記事をPost

Fig.4 タイトルが全部URLエンコードされたまま!!

ぎゃー!!

もう少し試してみる。

Fig.5 日経BPで試す

Fig.6 同じ結果

うーん。

Yahoo! ニュースならどうだ。

Fig.7 Yahoo! ニュースで試す

Fig.8 うまくいった…

うまくいった。
1文字目がASCII文字か非ASCII文字かで、Delicious APIの挙動が違うかな? もう少し調査が必要だ。
現時点でのソース
使い物にならんけど、晒しておく。
428行目を追加し、関数getEncodedStrを追加している(722~736行目)。
var INFO = xml`<plugin name=${NAME} version="0.19.0"
        href="https://github.com/vimpr/vimperator-plugins/raw/master/direct_bookmark.js"
        summary="Direct Post to Social Bookmarks"
        lang="en_US"
        xmlns="http://vimperator.org/namespaces/liberator">
    <project name="Vimperator" minVersion="3.6"/>
    <author email="trapezoid.g@gmail.com" homepage="http://unsigned.g.hatena.ne.jp/Trapezoid/">Trapezoid</author>
    <license>GPL</license>
    <p>Social Bookmark direct add script</p>
    <p>for Migemo search: require XUL/Migemo Extension</p>
    <h3 tag="directBookmark_Parts">Parts</h3>
    <ul>
        <li>http://d.hatena.ne.jp/fls/20080309/p1</li>
        <li>Pagerization (c) id:ofk</li>
        <li>AutoPagerize (c) id:swdyh</li>
        <li>direct_delb.js id:mattn</li>
        <li>JSDeferred id:cho45</li>
    </ul>
    <h3 tag="directBookmark_variables">Viriables</h3>
    <h4 tag="direct_sbm_use_services_by_tag">g:direct_sbm_use_services_by_tag</h4>
    <p>Use social bookmark services to extract tags</p>
    <dl>
        <dt>h</dt><dd>Hatena Bookmark</dd>
        <dt>d</dt><dd>del.icio.us</dd>
        <dt>l</dt><dd>livedoor clip</dd>
        <dt>g</dt><dd>Google Bookmarks</dd>
        <dt>p</dt><dd>Places (Firefox bookmarks)</dd>
        <dt>P</dt><dd>pinboard.in</dd>
    </dl>
    <p>Usage: <ex>let g:direct_sbm_use_services_by_tag = "hdl"</ex></p>

    <h4 tag="direct_sbm_use_services_by_post">g:direct_sbm_use_services_by_post</h4>
    <p>Use social bookmark services to post</p>
    <dl>
        <dt>h</dt><dd>Hatena Bookmark</dd>
        <dt>d</dt><dd>del.icio.us</dd>
        <dt>l</dt><dd>livedoor clip</dd>
        <dt>g</dt><dd>Google Bookmarks</dd>
        <dt>P</dt><dd>pinboard.in</dd>
    </dl>
    <p>Usage: <ex>let g:direct_sbm_use_services_by_post = "hdl"</ex></p>

    <h4 tag="direct_sbm_echo_type">g:direct_sbm_echo_type</h4>
    <p>Post message type</p>
    <dl>
        <dt>simple</dt><dd>single line, no posted services description</dd>
        <dt>multiline</dt><dd>multi line, display services description</dd>
        <dt>none</dt><dd>hide post message</dd>
    </dl>

    <h4 tag="direct_sbm_is_normalize">g:direct_sbm_is_normalize</h4>
    <p>Use normalize permalink</p>

    <h4 tag="direct_sbm_is_use_migemo">g:direct_sbm_is_use_migemo</h4>
    <p>Use Migemo completion</p>

    <h4 tag="direct_sbm_private">g:direct_sbm_private</h4>
    <p>Private bookmark</p>

    <item>
        <tags>:btags</tags>
        <spec>:btags</spec>
        <description>
            <p>Extract tags from social bookmarks for completion</p>
        </description>
    </item>
    <item>
        <tags>:sbm</tags>
        <spec>:sbm <oa>-s<oa>ervice</oa> <a>service</a></oa></spec>
        <description>
            <p>Post a current page to social bookmarks.</p>
            <p><a>service</a>: Specify target SBM services to post (default: "hdl")</p>
        </description>
    </item>
    <item>
        <tags>:bentry</tags>
        <spec>:bentry</spec>
        <description>
            <p>Goto Bookmark Entry Page</p>
        </description>
    </item>
    <item>
        <tags>:bicon</tags>
        <spec>:bicon</spec>
        <description>
            <p>Show Bookmark Count as Icon</p>
        </description>
    </item>
</plugin>`;

(function(){
    var evalFunc = window.eval;
    try {
        var sandbox = new Components.utils.Sandbox(window);
        if (Components.utils.evalInSandbox("true", sandbox) === true) {
            evalFunc = function(text) {
                return Components.utils.evalInSandbox(text, sandbox);
            }
        }
    } catch(e) { liberator.log('warning: direct_bookmark.js is working with unsafe sandbox.'); }

    var useServicesByPost = liberator.globalVariables.direct_sbm_use_services_by_post || 'hdl';
    var useServicesByTag = liberator.globalVariables.direct_sbm_use_services_by_tag || 'hdl';
    var echoType = liberator.globalVariables.direct_sbm_echo_type || 'multiline';
    var isNormalize = typeof liberator.globalVariables.direct_sbm_is_normalize == 'undefined' ? 
                      true : evalFunc(liberator.globalVariables.direct_sbm_is_normalize); 
    var isUseMigemo = typeof liberator.globalVariables.direct_sbm_is_use_migemo == 'undefined' ? 
                      true : evalFunc(liberator.globalVariables.direct_sbm_is_use_migemo);
    var isPrivate = typeof liberator.globalVariables.direct_sbm_private == 'undefined' ? 
                    false : evalFunc(liberator.globalVariables.direct_sbm_private);

    var XMigemoCore;
    try{
        XMigemoCore = Components.classes['@piro.sakura.ne.jp/xmigemo/factory;1']
                                .getService(Components.interfaces.pIXMigemoFactory)
                                .getService("ja");
    }
    catch(ex if ex instanceof TypeError){}


    function Deferred () this instanceof Deferred ? this.init(this) : new Deferred();
    Deferred.prototype = {
        init : function () {
            this._next    = null;
            this.callback = {
                ok: function (x) x,
                ng: function (x) { throw  x }
            };
            return this;
        },

        next  : function (fun) this._post("ok", fun),
        error : function (fun) this._post("ng", fun),
        call  : function (val) this._fire("ok", val),
        fail  : function (err) this._fire("ng", err),

        cancel : function () {
            (this.canceller || function () {})();
            return this.init();
        },

        _post : function (okng, fun) {
            this._next = new Deferred();
            this._next.callback[okng] = fun;
            return this._next;
        },

        _fire : function (okng, value) {
            var self = this, next = "ok";
            try {
                value = self.callback[okng].call(self, value);
            } catch (e) {
                next  = "ng";
                value = e;
            }
            if (value instanceof Deferred) {
                value._next = self._next;
            } else if (self._next) {
                self._next._fire(next, value);
            }
            return this;
        }
    };

    Deferred.next = function (fun) {
        var d = new Deferred();
        var id = setTimeout(function () { clearTimeout(id); d.call() }, 0);
        if (fun) d.callback.ok = fun;
        d.canceller = function () { try { clearTimeout(id) } catch (e) {} };
        return d;
    };

    function http (opts) {
        var d = Deferred();
        var req = new XMLHttpRequest();
        req.open(opts.method, opts.url, true, opts.user || null, opts.password || null);
        if (opts.headers) {
            for (var k in opts.headers) if (opts.headers.hasOwnProperty(k)) {
                req.setRequestHeader(k, opts.headers[k]);
            }
        }
        req.onreadystatechange = function () {
            if (req.readyState == 4) d.call(req);
        };
        req.send(opts.data || null);
        d.xhr = req;
        return d;
    }

    Deferred.Deferred = Deferred;
    Deferred.http     = http;


    function WSSEUtils(aUserName, aPassword){
        this._init(aUserName, aPassword);
    }

    WSSEUtils.prototype = {

        get userName() this._userName,
        get noce() this._nonce,
        get created() this._created,
        get passwordDigest() this._passwordDigest,

        getWSSEHeader: function(){
            var result = [
                'UsernameToken Username="' + this._userName + '", ',
                'PasswordDigest="' + this._passwordDigest + '=", ',
                'Nonce="' + this._nonce + '", ',
                'Created="' + this._created + '"'
            ].join("");

            return result;
        },

        _init: function(aUserName, aPassword){
            var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
            var seed = (new Date()).toUTCString() + uuidGenerator.generateUUID().toString();

            this._userName = aUserName;
            this._nonce = this._getSha1Digest(seed, true);
            this._created = this._getISO8601String((new Date()));
            this._passwordDigest = this._getSha1Digest(this._getSha1Digest(seed, false) + this._created + aPassword, true);
        },

        _getSha1Digest: function(aString, aBase64){
            var cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
            cryptoHash.init(Ci.nsICryptoHash.SHA1);

            var inputStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
            inputStream.setData(aString, aString.length);
            cryptoHash.updateFromStream(inputStream, -1);

            return cryptoHash.finish(aBase64);
        },

        _getISO8601String: function(aDate){
            var result = [
                zeropad(aDate.getUTCFullYear(), 4), "-",
                zeropad(aDate.getUTCMonth() + 1, 2), "-",
                zeropad(aDate.getUTCDate(), 2), "T",
                zeropad(aDate.getUTCHours(), 2), ":",
                zeropad(aDate.getUTCMinutes(), 2), ":",
                zeropad(aDate.getUTCSeconds(), 2), "Z"
            ].join("");
            return result;

            function zeropad(s, l){
                s = String(s);
                while(s.length < l){
                    s = "0" + s;
                }
                return s;
            }
        }

    };

    // copied from AutoPagerize (c) id:swdyh
    function getElementsByXPath(xpath, node){
        node = node || document;
        var nodesSnapshot = (node.ownerDocument || node).evaluate(xpath, node, null,
                XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        var data = [];
        for(var i = 0, l = nodesSnapshot.snapshotLength; i < l;
                data.push(nodesSnapshot.snapshotItem(i++)));
        return (data.length > 0) ? data : null;
    }

    function getFirstElementByXPath(xpath, node){
        node = node || document;
        var result = (node.ownerDocument || node).evaluate(xpath, node, null,
                XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        return result.singleNodeValue ? result.singleNodeValue : null;
    }

    // copied from http://d.hatena.ne.jp/odz/20060901/1157165797 id:odz
    function parseHTML(text) {
        var createHTMLDocument = function() {
            var xsl = (new DOMParser()).parseFromString(
                ['<?xml version="1.0"?>',
                 '<stylesheet version="1.0" xmlns="http://www.w3.org/1999/XSL/Transform">',
                 '<output method="html"/>',
                 '</stylesheet>'].join("\n"), "text/xml");

            var xsltp = new XSLTProcessor();
            xsltp.importStylesheet(xsl);
            var doc = xsltp.transformToDocument(
                document.implementation.createDocument("", "", null));
            return doc;
        };

        var doc = createHTMLDocument();
        var range = doc.createRange();
        doc.appendChild(doc.createElement("html"));
        range.selectNodeContents(doc.documentElement);
        doc.documentElement.appendChild(
            range.createContextualFragment(text));
        return doc;
    }

    function getNormalizedPermalink(url){
        var canonical = plugins.libly.$U.getFirstNodeFromXPath('//link[@rel="canonical"]');
        return canonical ? canonical.href : url;
    }

    function getUserAccount(form,post,arg){
        var user, password;
        try{
            var passwordManager = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
            var logins = passwordManager.findLogins({}, form, post, arg);
            if(logins.length > 0){
                [user, password] = [logins[0].username, logins[0].password];
            } else {
                var promptUser = { value : this.loginPrompt.user }, promptPass = { value : this.loginPrompt.password };
                var promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"]
                    .getService(Ci.nsIPromptService);

                var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
                        Ci.nsILoginInfo,
                        "init");

                var ret = promptSvc.promptUsernameAndPassword(
                    window, form, this.loginPrompt.description,
                    promptUser, promptPass, null, {}
                );
                if(ret){
                    [user, password] = [promptUser.value, promptPass.value];
                    var formLoginInfo = new nsLoginInfo(form,
                            post, null,
                            user, password, '', '');
                    passwordManager.addLogin(formLoginInfo);
                } else {
                    liberator.echoerr("Direct Social Bookmark: account not found - " + form);
                }
            }
        }
        catch(ex){
            liberator.echoerr("Direct Social Bookmark: handled exception during tag extracting");
            liberator.log(ex);
        }
        return [user, password];
    }

    var services = {
        'h': {
            description:'Hatena bookmark',
            account:['https://www.hatena.ne.jp', 'https://www.hatena.ne.jp', null],
            loginPrompt:{ user:'', password:'', description:'Enter username and password.' },
            entryPage:'http://b.hatena.ne.jp/entry/%URL::HATENA%',
            poster:function(user,password,url,title,comment,tags){
                var tagString = tags.length > 0 ? '[' + tags.join('][') + ']' : "";
                // TODO: xml`...` を使うとillegal character 言われる、、、何故?
                /*
                var request = xml`<entry xmlns="http://purl.org/atom/ns#">
                    <title>dummy</title>
                    <link rel="related" type="text/html" href=${url}/>
                    <summary type="text/plain">${tagString + comment}</summary>
                </entry>`;
                */
                var request = '<entry xmlns="http://purl.org/atom/ns#">' +
                        '<title>dummy</title>' +
                        '<link rel="related" type="text/html" href="' + url + '"/>' +
                        '<summary type="text/plain">' + tagString + comment + '</summary>' +
                    '</entry>';
                var wsse = new WSSEUtils(user,password);

                return Deferred.http({
                    method: "post",
                    url: "http://b.hatena.ne.jp/atom/post",
                    data: request.toString(),
                    headers: {
                        "X-WSSE": wsse.getWSSEHeader(),
                        "Content-Type": "application/atom+xml",
                    },
                }).next(function(xhr){
                    if(xhr.status != 201) throw "Hatena Bookmark: failed";
                });
            },
            tags:function(user,password){
                var xhr = new XMLHttpRequest();
                var hatena_tags = [];

                // http://b.hatena.ne.jp/retlet/20110322#bookmark-34906937
                xhr.open("GET","http://b.hatena.ne.jp/"+user+"/sidebar?with_tags=1",false);
                xhr.send(null);

                var mypage_html = parseHTML(xhr.responseText);
                var tags = getElementsByXPath('id("tags")/li/a', mypage_html);

                tags.forEach(function(tag){
                    hatena_tags.push(tag.innerHTML);
                });
                return hatena_tags;
            },
            userTags:function(url, results){
                var url = 'http://b.hatena.ne.jp/entry/jsonlite/?url=' + encodeURIComponent(url)

                return Deferred.http({
                    method: "get",
                    url: url,
                }).next(function(xhr){
                    if(xhr.status != 200)
                        return;
                    let json = JSON.parse(xhr.responseText);
                    if (!json)
                        return;
                    let tags = json.bookmarks.map(function(it) it.tags);
                    tags = tags.filter(function(it) it.length);
                    if (!tags.length)
                        return;
                    tags = Array.concat.apply([], tags);
                    tags = tags.map(String.trim);
                    tags = util.Array.uniq(tags);
                    results.push(tags);
                });
            },
            icon:function(url){
                return '<img src="http://b.hatena.ne.jp/entry/image/' + url + '" style="vertical-align: middle;" />';
            },
        },
        'd': {
            description:'del.icio.us',
            account:['https://secure.delicious.com', 'https://secure.delicious.com', null],
            loginPrompt:{ user:'', password:'', description:'Enter username and password.' },
            entryPage:'http://del.icio.us/url/%URL::MD5%',
            poster:function(user,password,url,title,comment,tags){
                title = getEncodedStr(title);
                var request_url = 'https://api.del.icio.us/v1/posts/add?' + [
                    ['url', url], ['description', title], ['extended', comment], ['tags', tags.join(',')]
                ].map(function(p) p[0] + '=' + encodeURIComponent(p[1])).join('&');
                return Deferred.http({
                    method: "get",
                    url: request_url,
                    user: user,
                    password: password,
                }).next(function(xhr){
                    if(xhr.status != 200) throw "del.icio.us: failed";
                });
            },
            tags:function(user,password){
                const url = 'https://api.del.icio.us/v1/tags/get?';
                var xhr = new XMLHttpRequest();
                xhr.open("GET", url, false, user, password);
                xhr.send(null);

                return [
                    e.getAttribute("tag")
                    for ([, e] in Iterator(Array.slice(xhr.responseXML.querySelectorAll('tag'))))
                ];
            },
            icon:function(url){
                var url = liberator.modules.buffer.URL;
                var cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
                cryptoHash.init(Ci.nsICryptoHash.MD5);
                var inputStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
                inputStream.setData(url, url.length);
                cryptoHash.updateFromStream(inputStream, -1);
                var hash = cryptoHash.finish(false), ascii = [];
                const hexchars = '0123456789ABCDEF';
                var hexrep = new Array(hash.length * 2);
                for(var i = 0; i < hash.length; i++) {
                    ascii[i * 2] = hexchars.charAt((hash.charCodeAt(i) >> 4) & 0xF);
                    ascii[i * 2 + 1] = hexchars.charAt(hash.charCodeAt(i) & 0xF);
                }
                return '<img src="http://del.icio.us/feeds/img/savedcount/' + ascii.join('').toLowerCase() + '?aggregate" style="vertical-align: middle;" />';
            },
        },
        'l': {
            description:'livedoor clip',
            account:['http://api.clip.livedoor.com', 'http://api.clip.livedoor.com', null],
            loginPrompt:{ user:'', password:'apikey', description:'Enter username and apikey.\nyou can get "api-key" from\n\thttp://clip.livedoor.com/config/api' },
            entryPage:'http://clip.livedoor.com/page/%URL%',
            poster:function(user,password,url,title,comment,tags){
                var rate=0;
                var starFullRate=5;
                if(comment.match(/\*+$/)){
                    comment = RegExp.leftContext;
                    rate = (RegExp.lastMatch.length > starFullRate)? starFullRate : RegExp.lastMatch.length;
                }
                var request_url = 'http://api.clip.livedoor.com/v1/posts/add?' + [
                    ['url', url], ['description', title], ['extended', comment], ['rate', rate], ['tags', tags.join(' ')], ['cache', (new Date()).getTime()]
                ].map(function(p) p[0] + '=' + encodeURIComponent(p[1])).join('&');
                return Deferred.http({
                    method: "get",
                    url: request_url,
                    user: user,
                    password: password,
                }).next(function(xhr){
                    if(xhr.status != 200) {
                        throw "livedoor clip: failed";
                    }
                });
            },
            tags:function(user,password){
                var xhr = new XMLHttpRequest();
                var ldc_tags = [];

                xhr.open("GET","http://clip.livedoor.com/clip/add?link=http://example.example/",false);
                xhr.send(null);

                var mypage_html = parseHTML(xhr.responseText);
                var tags = getElementsByXPath("id(\"tag_list\")/div/span",mypage_html);
                if (!tags)
                    return [];

                tags.forEach(function(tag){
                    ldc_tags.push(tag.textContent);
                });
                return ldc_tags;
            },
            icon:function(url){
                return '<img src="http://image.clip.livedoor.com/counter/' + url + '" style="vertical-align: middle;" />';
            },
        },
        'g': {
            description:'Google Bookmarks',
            account:null,
            loginPrompt:null,
            entryPage:'%URL%',
            poster:function(user,password,url,title,comment,tags){
                var request_url = 'http://www.google.com/bookmarks/mark';
                var params = [
                    ['bkmk', url], ['title', title], ['labels', tags.join(',')], ['annotation', comment]
                ].map(function(p) p[0] + '=' + encodeURIComponent(p[1])).join('&');
                return Deferred.http({
                    method: "post",
                    url: request_url,
                    data: params,
                    headers: {
                        "User-Agent": navigator.userAgent + " GoogleToolbarFF 3.0.20070525",
                    },
                }).next(function(xhr){
                    if(xhr.status != 200) throw "Google Bookmarks: failed";
                });
            },
            tags:function(user,password){
                return [];

                // FIXME: NOT WORKS
                //
                // var returnValue = [];
                // var xhr = new XMLHttpRequest();
                // xhr.open("GET", "https://www.google.com/bookmarks", false, user, password);
                // xhr.send(null);

                // var html = parseHTML(xhr.responseText);
                // var tags = getElementsByXPath('id("sidenav")/div/ul/li/a[count(*)=1]/text()',html);

                // tags.forEach(function(tag){
                //     returnValue.push(tag.textContent.match(/\S+/));
                // });
                // return returnValue;
            },
        },
        'f': {
            description:'foves',
            account:['https://secure.faves.com', 'https://secure.faves.com', null],
            loginPrompt:{ user:'', password:'', description:'Enter username and password.' },
            entryPage:'%URL%',
            poster:function(user,password,url,title,comment,tags){
                var request_url = 'https://secure.faves.com/v1/posts/add?' + [
                    ['url', url], ['description', title], ['extended', comment], ['tags', tags.join(' ')]
                ].map(function(p) p[0] + '=' + encodeURIComponent(p[1])).join('&');
                return Deferred.http({
                    method: "get",
                    url: request_url,
                    user: user,
                    password: password,
                }).next(function(xhr){
                    if(xhr.status != 200) throw "foves: failed";
                });
            },
            tags:function(user,password){
                const feed_url = 'https://secure.faves.com/v1/tags/get';
                var returnValue = [];
                var xhr = new XMLHttpRequest();
                xhr.open("GET", feed_url, false, user, password);
                xhr.send(null);

                var tags = xhr.responseXML.getElementsByTagName('tag');
                for(var n = 0; n < tags.length; n++)
                    returnValue.push(tags[n].getAttribute('tag'));
                return returnValue;
            },
        },
        'p': {
            description:'Places',
            account:null,
            loginPrompt:null,
            entryPage:'%URL%',
            poster:function(user,password,url,title,comment,tags){
                const taggingService = Cc["@mozilla.org/browser/tagging-service;1"].getService(Ci.nsITaggingService);
                var nsUrl = Cc["@mozilla.org/network/standard-url;1"].createInstance(Ci.nsIURL);
                nsUrl.spec = url;
                taggingService.tagURI(nsUrl,tags);
                try{
                    Application.bookmarks.tags.addBookmark(title, nsUrl);
                }catch(e){
                    throw "Places: failed";
                }
            },
            tags:function(user,password)
                Application.bookmarks.tags.children.map(function(x) x.title),
        },
        'P': {
            description:'pinboard',
            account:['https://pinboard.in', 'https://pinboard.in', null],
            loginPrompt:{ user:'', password:'', description:'Enter username and password.' },
            entryPage:'%URL%',
            poster:function(user,password,url,title,comment,tags){
                var rate=0;
                var starFullRate=5;
                if(comment.match(/\*+$/)){
                    comment = RegExp.leftContext;
                    rate = (RegExp.lastMatch.length > starFullRate)? starFullRate : RegExp.lastMatch.length;
                }
                var request_url = 'https://api.pinboard.in/v1/posts/add?' + [
                    ['url', url], ['description', title], ['extended', comment], ['tags', tags.join(' ')],
                    ['shared', isPrivate ? 'no' : 'yes'], ['toread', 'no'],
                ].map(function(p) p[0] + '=' + encodeURIComponent(p[1])).join('&');
                return Deferred.http({
                    method: "get",
                    url: request_url,
                    user: user,
                    password: password,
                }).next(function(xhr){
                    if(xhr.status != 200) {
                        throw "pinboard.in: failed";
                    }
                });
            },
            tags:function(user,password){
                const url = 'https://api.pinboard.in/v1/tags/get?';
                var xhr = new XMLHttpRequest();
                xhr.open("GET", url, false, user, password);
                xhr.send(null);

                return [
                    e.getAttribute("tag")
                    for ([, e] in Iterator(Array.slice(xhr.responseXML.querySelectorAll('tag'))))
                ];
            },
            //icon:function(url){
            //    return '<img src="http://image.clip.livedoor.com/counter/' + url + '" style="vertical-align: middle;" />';
            //},
        },
    };
    __context__.services = services;

    let (_tags = {}, _empty = true) {
        __context__.tags = {
            __iterator__: function () Iterator(_tags, true),
            update: function (atags) {
                _tags = {};
                _empty = atags.length === 0;
                for (let [, t] in Iterator(atags))
                    _tags[t] = true;
            },
            add: function (newTag) {
                _tags[newTag] = true;
            },
            get isEmpty () _empty,
        };
    }

    function getTagsAsync(onComplete){
        var d,first;
        d = first = Deferred();

        useServicesByTag.split(/\s*/).forEach(function(service){
            var user, password, currentService = services[service] || null;
            [user,password] = currentService.account ? getUserAccount.apply(currentService,currentService.account) : ["", ""];
            d = d.next(function(t) {
                var tags = currentService.tags(user,password);
                liberator.echo(currentService.description + ": Tag parsing is finished. Taglist length: " + tags.length);
                return t.concat(tags);
            });
        });
        d.next(function(tags){
            tags = tags.filter(function(e,i,a) a.indexOf(e) == i);
            tags.sort();
            __context__.tags.update(tags);
            if (onComplete)
                onComplete(tags);
        }).error(function(e){liberator.echoerr(e, null, "direct_bookmark.js: ")});
        return first;
    }
    function getUserTags(url, onComplete){
        var d = new Deferred();
        var first = d;
        var results = [];

        useServicesByTag.split(/\s*/).forEach(function(service){
            var user, password, currentService = services[service] || null;
            if (!(currentService && currentService.userTags))
                return;
            d = d.next(currentService.userTags(url, results));
        });
        d.next(function(){
            let tags = results.length ? Array.concat.apply([], results) : [];
            onComplete(tags);
        });

        first.call([]);
    }
    function getTitleByURL(url) {
        if (url === liberator.modules.buffer.URL)
            return liberator.modules.buffer.title;

        let xhr = new XMLHttpRequest();
        xhr.open("GET", url, false);
        xhr.send(null);

        let html = parseHTML(xhr.responseText);
        let title = getFirstElementByXPath("//title/text()", html);

        // FIXME: encoding (see charset and must I convert...?)
        return title.nodeValue;
    }

    function getEncodedStr(aString) {
        let rv = "";
        let strArray = aString.split("");

        if (typeof(aString) === "string") {
            for (let i = 0; i < aString.length; i++) {
                if (aString.charCodeAt(i) > 127) {
                    rv += encodeURIComponent(strArray[i]);
                } else {
                    rv += strArray[i];
                }
            }
        }
        return rv;
    }
    liberator.modules.commands.addUserCommand(['btags'],"Update Social Bookmark Tags",
        function(arg){setTimeout(function(){getTagsAsync().call([])},0)}, {}, true);
    liberator.modules.commands.addUserCommand(['bentry'],"Goto Bookmark Entry Page",
        function(args){
            var service = args.string || useServicesByPost.split(/\s*/)[0];
            var currentService = services[service] || null;
            if(!currentService || !currentService.entryPage) {
                return;
            }
            liberator.open(currentService.entryPage
                .replace(/%URL(?:::(HATENA|ESC|MD5))?%/g, function(x, t){
                    if(!t) return liberator.modules.buffer.URL.replace(/#/, '%23');
                    if(t == "HATENA") return liberator.modules.buffer.URL.replace(/^http:\/\//, '').replace(/^https:\/\//, 's/').replace(/#/, '%23');
                    if(t == "ESC") return encodeURIComponent(liberator.modules.buffer.URL);
                    if(t == "MD5"){
                        var url = liberator.modules.buffer.URL;
                        var cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
                        cryptoHash.init(Ci.nsICryptoHash.MD5);
                        var inputStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
                        inputStream.setData(url, url.length);
                        cryptoHash.updateFromStream(inputStream, -1);
                        var hash = cryptoHash.finish(false), ascii = [];
                        const hexchars = '0123456789ABCDEF';
                        var hexrep = new Array(hash.length * 2);
                        for(var i = 0; i < hash.length; i++) {
                            ascii[i * 2] = hexchars.charAt((hash.charCodeAt(i) >> 4) & 0xF);
                            ascii[i * 2 + 1] = hexchars.charAt(hash.charCodeAt(i) & 0xF);
                        }
                        return ascii.join('').toLowerCase();
                    }
                }), args.bang ? liberator.NEW_TAB : liberator.CURRENT_TAB);
        },{
            completer: function(filter)
                [0, useServicesByPost.split(/\s*/).map(function(p) [p, services[p].description])]
        },
        true
    );
    liberator.modules.commands.addUserCommand(['bicon'],"Show Bookmark Count as Icon",
        function(arg){
            var url = getNormalizedPermalink(liberator.modules.buffer.URL);
            var html = useServicesByTag.split(/\s*/).map(function(service){
                var currentService = services[service] || null;
                return (currentService && typeof currentService.icon === 'function') ?
                        (currentService.description + ': ' + currentService.icon(url)) : null;
            }).join('<br />');
            liberator.echo(html, true);
        }, {}, true);
    // Add :sbm, :sbmo {{{
    {
        let makeAction = function(withUrl) {
            return function(arg){
                var targetServices = useServicesByPost;
                var url = liberator.modules.buffer.URL;

                if (arg["-s"]) targetServices = arg["-s"];
                if (arg[0] && withUrl) url = arg[0];
                var comment = arg.literalArg;

                var tags = [];
                var re = /\[([^\]]+)\]([^\[].*)?/g;

                var d = new Deferred();
                var first = d;

                if(/^\[[^\]]+\]/.test(comment)){
                    var tag, text;
                    while((tag = re.exec(comment))){
                        [, tag, text] = tag;
                        tags.push(tag);
                    }
                    comment = text || '';
                }

                tags.forEach(function (t) __context__.tags.add(t));

                targetServices.split(/\s*/).forEach(function(service){
                    var user, password, currentService = services[service] || null;
                    [user,password] = currentService.account ? getUserAccount.apply(currentService,currentService.account) : ["", ""];
                    d = d.next(function() currentService.poster(
                        user,password,
                        isNormalize ? getNormalizedPermalink(url) : url,getTitleByURL(url),
                        comment,tags
                        ));
                    if(echoType == "multiline") {
                        d = d.next(function(){
                            liberator.echo("[" + services[service].description + "] post completed.");
                        });
                    }
                    d = d.error(function() {
                        liberator.echoerr(services[service].description + ": failed");
                    });
                });
                if(echoType == "simple") {
                    d = d.next(function(){
                        liberator.echo("post completed.");
                    });
                }
                d.error(function(e){liberator.echoerr("direct_bookmark.js: Exception throwed! " + e);liberator.log(e);});
                setTimeout(function(){first.call();},0);
            };
        };

        let completer = let (lastURL, lastUserTags, onComplete, done = true) function(context, arg){
            function matchPosition (e){
                let m = liberator.globalVariables.direct_sbm_tag_match || 'prefix';
                switch (m) {
                case 'infix': return e;
                case 'suffix': return e + "$";
                }
                return "^" + e;
            }

            function set (context, tags) {
                let filter = context.filter;
                var match_result = filter.match(/((?:\[[^\]]*\])*)\[?(.*)/); //[all, commited, now inputting]
                var expr = XMigemoCore && isUseMigemo ? "(" + XMigemoCore.getRegExp(match_result[2]) + ")"
                                                      : match_result[2];
                var m = new RegExp(matchPosition(expr),'i');

                context.advance( match_result[1].length );

                context.incomplete = false;
                context.completions =
                    [ ["[" + tag + "]","Tag"]
                      for each (tag in tags)
                      if (m.test(tag) && match_result[1].indexOf('[' + tag + ']') < 0) ];
            }

            let url = arg[0] || buffer.URL;
            liberator.log(url);

            context.fork('UserTags', 0, context, function(context){
                context.title = ['User Tags', 'User Tags'];

                onComplete = function(tags){
                    done = true;
                    lastUserTags = tags;
                    context.incomplete = false;
                    set(context, tags);
                };

                if (url == lastURL){
                    if (done) {
                        onComplete(lastUserTags);
                    } else {
                        context.incomplete = true;
                    }
                } else {
                    lastURL = url;
                    context.incomplete = true;
                    done = false;
                    getUserTags(url, function (tags) onComplete(tags));
                }
            });

            context.fork('MyTags', 0, context, function(context, arg){
                context.title = ['My Tag','Description'];

                if(__context__.tags.isEmpty){
                    context.incomplete = true;
                    getTagsAsync(set.bind(null, context)).call([]);
                } else {
                    set(context, __context__.tags);
                }
            });
        };

        let options = [ [['-s','-service'], liberator.modules.commands.OPTION_STRING] ];

        let urlCompleter = function(context, args){
            if (args.length <= 1) {
                return completion.url(context, 'hsl');
            } else {
                return completer(context, args);
            }
        };

        liberator.modules.commands.addUserCommand(['sbm'],"Post to Social Bookmark (Current Buffer)",
            makeAction(false),
            {literal: 0, completer: completer, options: options},
            true
        );

        liberator.modules.commands.addUserCommand(['sbmo[ther]'],"Post to Social Bookmark",
            makeAction(true),
            {literal: 1, completer: urlCompleter, options: options},
            true
        );
    } // }}}
})();

0 コメント:

コメントを投稿