root/lang/javascript/vimperator-plugins/trunk/sbmcommentsviewer.js

Revision 37447, 19.5 kB (checked in by anekos, 4 months ago)

空の時に死ぬで

Line 
1var PLUGIN_INFO =
2<VimperatorPlugin>
3    <name>SBM Comments Viewer</name>
4    <description>List show Social Bookmark Comments</description>
5    <description lang="ja">ソーシャル・ブックマーク・コメントを表示します</description>
6    <version>0.1.1</version>
7    <minVersion>2.0pre</minVersion>
8    <maxVersion>2.3</maxVersion>
9    <updateURL>http://svn.coderepos.org/share/lang/javascript/vimperator-plugins/trunk/sbmcommentsviewer.js</updateURL>
10    <detail><![CDATA[
11== Usage ==
12>||
13viewSBMComments [url] [options]
14 url             : 省略時は現在のURL
15 options:
16     -f, -format : 出力時のフォーマット(`,'区切りのリスト)
17                   (default: id,timestamp,tags,comment)
18                   let g:def_sbm_format = ... で指定可能
19     -t, -type   : 出力するSBMタイプ
20                   (default: hdl)
21                   let g:def_sbms = ... で指定可能
22     -c, -count  : ブックマーク件数のみ出力
23     -b, -browser: バッファ・ウィンドウではなくブラウザに開く
24                   TODO:まだ出来てない
25||<
26
27== 指定可能フォーマット ==
28  id, timpstamp, tags, comment, tagsAndComment
29
30== SBMタイプ ==
31- h : hatena bookmark
32- d : Delicious
33- l : livedoor clip
34- z : Buzzurl
35- XXX:今後増やしていきたい
36
37>||
38e.g.)
39  :viewSBMComments http://d.hatena.ne.jp/teramako/ -t hdl -f id,comment -c
40||<
41
42== 備考 ==
43 一度取得したものは(30分ほど)キャッシュに貯めてますので何度も見直すことが可能です。
44 粋なコマンド名募集中
45     ]]></detail>
46</VimperatorPlugin>;
47liberator.plugins.sbmCommentsViewer = (function(){
48
49var isFilterNoComments = liberator.globalVariables.sbm_comments_viewer_filter_nocomments || false;
50
51/**
52 * SBMEntry Container {{{
53 * @param {String} type
54 * @param {Number} count
55 * @param {Object} extra
56 *  extra = {
57 *      faviconURL,
58 *      pageURL
59 *  }
60 */
61function SBMContainer(type, count, extra){ //{{{
62    this.type = type;
63    this.count = count || 0;
64    this.entries = [];
65    if (extra){
66        this.faviconURL = extra.faviconURL || '';
67        this.pageURL = extra.pageURL || '';
68    }
69} //}}}
70SBMContainer.prototype = { //{{{
71    add: function(id, timestamp, comment, tags, extra){
72        this.entries.push(new SBMEntry(
73            id, timestamp, comment, tags, extra
74        ));
75    },
76    toHTML: function(format, countOnly){
77        var label = <>
78            {this.faviconURL ? <img src={this.faviconURL} width="16" height="16"/> : <></>}
79            {manager.type[this.type] + ' ' + this.count + '(' + this.entries.length + ')'}
80            {this.pageURL ? <a href="#">{this.pageURL}</a> : <></>}
81        </>;
82        if (countOnly){
83            return label;
84        } else {
85            let xml = <table id="liberator-sbmcommentsviewer">
86                <caption style="text-align:left" class="hl-Title">{label}</caption>
87            </table>;
88            let self = this;
89            xml.* += (function(){
90                var thead = <tr/>;
91                format.forEach(function(colum){ thead.* += <th>{manager.format[colum] || '-'}</th>; });
92                var tbody = <></>;
93                self.entries.forEach(function(e){
94                    if (isFilterNoComments && !e.comment) return;
95                    tbody += e.toHTML(format);
96                });
97                return thead + tbody;
98            })();
99            return xml;
100        }
101    }
102}; //}}}
103// }}}
104/**
105 * SBM Entry {{{
106 * @param {String} id UserName
107 * @param {String|Date} timestamp
108 * @param {String} comment
109 * @param {String[]} tags
110 * @param {Object} extra
111 *  extra = {
112 *      userIcon
113 *      link
114 *  }
115 */
116function SBMEntry(id, timestamp, comment, tags, extra){ //{{{
117    this.id = id || '';
118    this.timeStamp = timestamp instanceof Date ? timestamp : null;
119    this.comment = comment || '';
120    this.tags = tags || [];
121    if (extra){
122        this.userIcon = extra.userIcon || null;
123        this.link     = extra.link     || null;
124    }
125} //}}}
126SBMEntry.prototype = { //{{{
127    toHTML: function(format){
128        var xml = <tr/>;
129        var self = this;
130        format.forEach(function(colum){
131            switch(colum){
132                case 'id':
133                    xml.* += <td class="liberator-sbmcommentsviewer-id">
134                                {self.userIcon ? <><img src={self.userIcon} width="16" height="16"/> {self.id}</> : <span>{self.id}</span>}
135                             </td>;
136                    break;
137                case 'timestamp':
138                    xml.* += <td class="liberator-sbmcommentsviewer-timestamp">{self.formatDate()}</td>; break;
139                case 'tags':
140                    xml.* += <td class="liberator-sbmcommentsviewer-tags">{self.tags.join(',')}</td>; break;
141                case 'comment':
142                    xml.* += <td class="liberator-sbmcommentsviewer-comment" style="white-space:normal;">{self.comment}</td>; break;
143                case 'tagsAndComment':
144                    var tagString = self.tags.length ? '[' + self.tags.join('][') + ']':'';
145                    xml.* += <td class="liberator-sbmcommentsviewer-tagsAndComment" style="white-space:normal;">{tagString + ' '+self.comment}</td>;
146                    break;
147                default:
148                    xml.* += <td>-</td>;
149            }
150        });
151        return xml;
152    },
153    formatDate: function(){
154        if (!this.timeStamp) return '';
155        var [year,month,day,hour,min,sec] = [
156            this.timeStamp.getFullYear(),
157            this.timeStamp.getMonth()+1,
158            this.timeStamp.getDate(),
159            this.timeStamp.getHours(),
160            this.timeStamp.getMinutes(),
161            this.timeStamp.getSeconds()
162        ];
163        return [
164            year, '/',
165            (month < 10 ? '0'+month : month), '/',
166            (day < 10 ? '0'+day : day), ' ',
167            (hour < 10 ? '0'+hour : hour), ':',
168            (min < 10 ? '0'+min : min), ':',
169            (sec < 10 ? '0'+sec : sec)
170        ].join('');
171    }
172}; //}}}
173//}}}
174/**
175 * openSBM {{{
176 * @param {String} url
177 * @param {String} type
178 * @param {String[]} format
179 * @param {Boolean} countOnly
180 * @param {Boolean} openToBrowser
181 */
182function openSBM(url, type, format, countOnly, openToBrowser){
183    var sbmLabel = manager.type[type];
184    var sbmURL = SBM[sbmLabel].getURL(url);
185    var xhr = new XMLHttpRequest();
186    xhr.open('GET', sbmURL, true);
187    xhr.onreadystatechange = function(){
188        if (xhr.readyState == 4){
189            if (xhr.status == 200){
190                let sbmContainer = SBM[sbmLabel].parser.call(this, xhr);
191                if (!sbmContainer) return;
192                cacheManager.add(sbmContainer, url, type);
193                if (openToBrowser)
194                    manager.open(sbmContainer.toHTML(format,false));
195                else
196                    liberator.echo(sbmContainer.toHTML(format,countOnly), true);
197            } else {
198                liberator.echoerr(sbmURL + ' ' + xhr.status, true);
199            }
200        }
201    };
202    xhr.send(null);
203} //}}}
204/**
205 * getURL と parser メソッドを供えること
206 * getURL は 取得先のURLを返すこと
207 * parser は SBMContainer オブジェクトを返すこと
208 */
209var SBM = { //{{{
210    hatena: { //{{{
211        getURL: function(url){
212            var urlPrefix = 'http://b.hatena.ne.jp/entry/jsonlite/?url=';
213            return urlPrefix + encodeURIComponent(url.replace(/%23/g,'#'));
214        },
215        parser: function(xhr){
216            //var json = window.eval(xhr.responseText);
217            var json = jsonDecode(xhr.responseText, false);
218            var count = json.bookmarks.length;
219            var c = new SBMContainer('h', json.count, {
220                faviconURL:'http://b.hatena.ne.jp/favicon.ico',
221                pageURL:   'http://b.hatena.ne.jp/entry/' + json.url
222            });
223            json.bookmarks.forEach(function(bm){
224                c.add(bm.user, new Date(bm.timestamp), bm.comment, bm.tags, {
225                    userIcon: 'http://www.hatena.ne.jp/users/' + bm.user.substring(0,2) + '/' + bm.user +'/profile_s.gif'
226                });
227            });
228            return c;
229        }
230    }, //}}}
231    delicious: { //{{{
232        getURL: function(url){
233            //var urlPrefix = 'http://del.icio.us/rss/url/';
234            var urlPrefix = 'http://feeds.delicious.com/rss/url/';
235            return urlPrefix + getMD5Hash(url);
236        },
237        parser: function(xhr){
238            var rss = xhr.responseXML;
239            if (!rss){
240                liberator.echoerr('Delicious feed is none',true);
241                return;
242            }
243            var pageURL, items;
244            try {
245                pageURL = evaluateXPath(rss, '//rss:channel/rss:link')[0].textContent;
246                items = evaluateXPath(rss, '//rss:item');
247            } catch(e){
248                liberator.log(e);
249            }
250            var c = new SBMContainer('d', items.length, {
251                faviconURL: 'http://delicious.com/favicon.ico',
252                pageURL:    pageURL
253            });
254            items.forEach(function(item){
255                var children = item.childNodes;
256                var [id,date,tags,comment,link] = ['','',[],'',''];
257                for (let i=0; i<children.length; i++){
258                    let node = children[i];
259                    if (node.nodeType == 1){
260                        switch (node.localName){
261                            case 'creator': id = node.textContent; break;
262                            case 'link': link = node.textContent; break;
263                            case 'date':
264                                date = stringToDate(node.textContent);
265                                break;
266                            case 'description': comment = node.textContent; break;
267                            case 'subject': tags = node.textContent.split(/\s+/); break;
268                        }
269                    }
270                }
271                c.add(id, date, comment, tags, {link: link});
272            });
273            return c;
274        }
275    }, //}}}
276    livedoorclip: { //{{{
277        getURL: function(url){
278            var urlPrefix = 'http://api.clip.livedoor.com/json/comments?link=';
279            return urlPrefix + encodeURIComponent(url.replace(/%23/g,'#')) + '&all=0';
280        },
281        parser: function(xhr){
282        /*
283            var json = Components.classes['@mozilla.org/dom/json;1'].
284                       getService(Components.interfaces.nsIJSON).
285                       decode(xhr.responseText);
286        */
287            var json = jsonDecode(xhr.responseText);
288            if (json && json.isSuccess){
289                let c = new SBMContainer('l', json.total_clip_count, {
290                    faviconURL: 'http://clip.livedoor.com/favicon.ico',
291                    pageURL:    'http://clip.livedoor.com/page/' + json.link
292                });
293                json.Comments.forEach(function(clip){
294                    c.add( clip.livedoor_id, new Date(clip.created_on * 1000),
295                           clip.notes ? clip.notes : '',
296                           clip.tags,
297                           {
298                            userIcon: 'http://image.clip.livedoor.com/profile/' +
299                                      '?viewer_id=[%%20member.livedoor_id%20Z%]&target_id=' +
300                                      clip.livedoor_id,
301                            link: 'http://clip.livedoor.com/clips/' + clip.livedoor_id
302                           }
303                    );
304                });
305                return c;
306            } else {
307                liberator.log('Faild: LivedoorClip');
308            }
309        }
310    }, //}}}
311    buzzurl: { //{{{
312        getURL: function(url){
313            var urlPrefix = 'http://api.buzzurl.jp/api/posts/get/v1/json/?url=';
314            return urlPrefix + encodeURIComponent(url.replace(/%23/g,'#'));
315        },
316        parser: function(xhr){
317            var url = 'http://buzzurl.jp/user/';
318            var json = jsonDecode(xhr.responseText);
319            if (json && json[0] && json[0].user_num){
320                let c = new SBMContainer('buzzurl', json[0].user_num, {
321                    faviconURL: 'http://buzzurl.jp/favicon.ico',
322                    pageURL:    'http://buzzurl.jp/entry/' + json[0].url
323                });
324                json[0].posts.forEach(function(entry){
325                    c.add( entry.user_name, stringToDate(entry.date),
326                           entry.comment ? entry.comment : '', (entry.keywords || '').split(','),
327                           {
328                            userIcon: url + entry.user_name + '/photo',
329                            link: url + '/' + entry.user_name
330                           }
331                    );
332                });
333                return c;
334            } else {
335                liberator.log('Faild: Buzzurl');
336            }
337        }
338    } //}}}
339}; //}}}
340
341/**
342 * jsonDecode {{{
343 * @param {String} str JSON String
344 * @param {Boolean} toRemove はてなブックマークのJSONの様に
345 *                           前後に()が付いている場合に取り除くためのフラグ
346 */
347function jsonDecode(str, toRemove){
348    var json = Components.classes['@mozilla.org/dom/json;1'].getService(Components.interfaces.nsIJSON);
349    if (toRemove) str = str.substring(1, str.length -1);
350
351    return json.decode(str);
352}
353//}}}
354/**
355 * getMD5Hash {{{
356 * @param {String} str
357 * @return {String} MD5HashString
358 */
359function getMD5Hash(str){
360    var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
361                    createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
362    converter.charset = 'UTF-8';
363    var result = {};
364    var data = converter.convertToByteArray(str, result);
365    var ch = Components.classes['@mozilla.org/security/hash;1'].createInstance(Components.interfaces.nsICryptoHash);
366    ch.init(ch.MD5);
367    ch.update(data, data.length);
368    var hash = ch.finish(false);
369    function toHexString(charCode){
370        return ('0' + charCode.toString(16)).slice(-2);
371    }
372    var s = [i < hash.length ? toHexString(hash.charCodeAt(i)) : '' for (i in hash)].join('');
373    return s;
374} //}}}
375/**
376 * stringToDate {{{
377 * @param {String} Date String
378 * @return {Date}
379 */
380function stringToDate(str){
381    let args = str.split(/[-T:Z]/,6).map(function (v) parseInt(v, 10));
382    args[1]--;
383    return new Date(args[0], args[1], args[2], args[3], args[4], args[5]);
384} //}}}
385/**
386 * evaluateXPath {{{
387 * @param {Element} aNode
388 * @param {String} aExpr XPath Expression
389 * @return {Element[]}
390 * @see http://developer.mozilla.org/ja/docs/Using_XPath
391 */
392function evaluateXPath(aNode, aExpr){
393    var xpe = new XPathEvaluator();
394    function nsResolver(prefix){
395        var ns = {
396            xhtml:   'http://www.w3.org/1999/xhtml',
397            rdf:     'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
398            dc:      'http://purl.org/dc/elements/1.1/',
399            rss:     'http://purl.org/rss/1.0/',
400            taxo:    'http://purl.org/rss/1.0/modules/taxonomy/',
401            content: 'http://purl.org/rss/1.0/modules/content/',
402            syn:     'http://purl.org/rss/1.0/modules/syndication/',
403            admin:   'http://webns.net/mvcb/'
404        };
405        return ns[prefix] || null;
406    }
407    var result = xpe.evaluate(aExpr, aNode, nsResolver, 0, null);
408    var found = [];
409    var res;
410    while (res = result.iterateNext())
411        found.push(res);
412    return found;
413} //}}}
414/**
415 * sbmCommentsView manager {{{
416 * @alias liberator.plugins.sbmCommentsViewer
417 */
418var manager = {
419    type: {
420        h: 'hatena',
421        d: 'delicious',
422        l: 'livedoorclip',
423        z: 'buzzurl'
424    },
425    format: {
426        id: 'ID',
427        comment: 'Comment',
428        timestamp: 'TimeStamp',
429        tags: 'Tags',
430        tagsAndComment: 'Tags&Comment'
431    },
432    // for debug
433    convertMD5: function(str){
434        return getMD5Hash(str);
435    },
436    // for debug
437    getXML: function(url){
438        var xhr = new XMLHttpRequest();
439        xhr.open('GET',url,false);
440        xhr.send(null);
441        return xhr;
442    },
443    // for debug
444    get cache(){
445        return cacheManager;
446    },
447    /**
448     * @param {String} str
449     * @param {Number} where
450     * TODO
451     */
452    open: function(str, where){
453        /*
454        getBrowser().addTab('data:text/html,'+str, null,null,null);
455        */
456    }
457}; //}}}
458
459var options = [
460    [['-type','-t'], commands.OPTION_STRING, function(str) (new RegExp('^['+[t for(t in manager.type)].join('') + ']+$')).test(str)],
461    [['-format','-f'], commands.OPTION_LIST,null, [[f,manager.format[f]] for (f in manager.format)]],
462    [['-count','-c'], commands.OPTION_NOARG],
463    [['-browser','-b'],commands.OPTION_NORARG]
464];
465commands.addUserCommand(['viewSBMComments'], 'SBM Comments Viewer', //{{{
466    function(arg){ //{{{
467        var types =  liberator.globalVariables.def_sbms || 'hdlz';
468        var format = (liberator.globalVariables.def_sbm_format || 'id,timestamp,tags,comment').split(',');
469        var countOnly = false, openToBrowser = false;
470        var url = buffer.URL;
471        [
472            let (v = arg['-' + name]) (v && f(v))
473            for ([name, f] in Iterator({
474                count: function () countOnly = true,
475                browser: function () openToBrowser = true,
476                type: function (v) (types = v),
477                format: function (v) (format = v),
478                arguments: function (v) (v.length > 0 && (url = v[0]))
479            }))
480        ]
481
482        for (let i=0; i<types.length; i++){
483            let type = types.charAt(i);
484            if ( manager.type[type] ){
485                if ( cacheManager.isAvailable(url, type) ){
486                    liberator.log('cache avairable');
487                    if (openToBrowser)
488                        // TODO
489                        manager.open(cacheManager.get(url,type).toHTML(format,false), liberator.forceNewTab);
490                    else
491                        liberator.echo(cacheManager.get(url, type).toHTML(format,countOnly), true);
492                } else {
493                    try {
494                        openSBM(url, type, format, countOnly, openToBrowser);
495                    } catch(e){
496                        liberator.log(e);
497                    }
498                }
499            }
500        }
501    }, //}}}
502    {
503        argCount:"*",
504        options: options,
505        completer: function(context) completion.url(context, 'l')
506    },
507    true
508); //}}}
509
510/**
511 * cacheManager {{{
512 */
513var cacheManager = (function(){
514    var cache = {};
515    //             min  sec   millisec
516    var threshold = 30 * 60 * 1000;
517    var interval  = 10 * 60 * 1000;
518    var c_manager = {
519        get raw(){
520            return cache;
521        },
522        has: function(url){
523            if (cache[url])
524                return true;
525            else
526                return false;
527        },
528        add: function(sbmComments, url, type){
529            if (!cache[url]) cache[url] = {};
530            cache[url][type] = [new Date(), sbmComments];
531        },
532        get: function(url, type){
533            return cache[url][type][1];
534        },
535        delete: function(url, type) {
536            if (!cache[url]) return true;
537            if (!type) return delete cache[url];
538            return delete cache[url][type];
539        },
540        garbage: function(){
541            var date = new Date();
542            for (let url in cache){
543                for (let type in cache[url]){
544                    if (date - cache[url][type][0] > threshold) delete cache[url][type];
545                }
546            }
547        },
548        isAvailable: function(url, type){
549            if (cache[url] && cache[url][type] && new Date() - cache[url][type][0] < threshold)
550                return true;
551
552            return false;
553        }
554    };
555    return c_manager;
556})();
557//}}}
558
559return manager;
560})();
561// vim: sw=4 ts=4 sts=0 et fdm=marker:
Note: See TracBrowser for help on using the browser.