root/lang/javascript/vimperator-plugins/branches/2.1/sbmcommentsviewer.js

Revision 28053, 19.5 kB (checked in by suVene, 20 months ago)

* fixed typo.

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.1c</version>
7    <minVersion>2.0pre</minVersion>
8    <maxVersion>2.0pre</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/json/?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, true);
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 = window.eval('new Date(' + node.textContent.split(/[-T:Z]/,6).join(',') + ')');
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, window.eval('new Date(' + entry.date.split(/[-\s:]/,6).join(',') + ')'),
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 * evaluateXPath {{{
377 * @param {Element} aNode
378 * @param {String} aExpr XPath Expression
379 * @return {Element[]}
380 * @see http://developer.mozilla.org/ja/docs/Using_XPath
381 */
382function evaluateXPath(aNode, aExpr){
383    var xpe = new XPathEvaluator();
384    function nsResolver(prefix){
385        var ns = {
386            xhtml:   'http://www.w3.org/1999/xhtml',
387            rdf:     'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
388            dc:      'http://purl.org/dc/elements/1.1/',
389            rss:     'http://purl.org/rss/1.0/',
390            taxo:    'http://purl.org/rss/1.0/modules/taxonomy/',
391            content: 'http://purl.org/rss/1.0/modules/content/',
392            syn:     'http://purl.org/rss/1.0/modules/syndication/',
393            admin:   'http://webns.net/mvcb/'
394        };
395        return ns[prefix] || null;
396    }
397    var result = xpe.evaluate(aExpr, aNode, nsResolver, 0, null);
398    var found = [];
399    var res;
400    while (res = result.iterateNext())
401        found.push(res);
402    return found;
403} //}}}
404/**
405 * sbmCommentsView manager {{{
406 * @alias liberator.plugins.sbmCommentsViewer
407 */
408var manager = {
409    type: {
410        h: 'hatena',
411        d: 'delicious',
412        l: 'livedoorclip',
413        z: 'buzzurl'
414    },
415    format: {
416        id: 'ID',
417        comment: 'Comment',
418        timestamp: 'TimeStamp',
419        tags: 'Tags',
420        tagsAndComment: 'Tags&Comment'
421    },
422    // for debug
423    convertMD5: function(str){
424        return getMD5Hash(str);
425    },
426    // for debug
427    getXML: function(url){
428        var xhr = new XMLHttpRequest();
429        xhr.open('GET',url,false);
430        xhr.send(null);
431        return xhr;
432    },
433    // for debug
434    get cache(){
435        return cacheManager;
436    },
437    /**
438     * @param {String} str
439     * @param {Number} where
440     * TODO
441     */
442    open: function(str, where){
443        /*
444        getBrowser().addTab('data:text/html,'+str, null,null,null);
445        */
446    }
447}; //}}}
448
449var options = [
450    [['-type','-t'], commands.OPTION_STRING, function(str) (new RegExp('^['+[t for(t in manager.type)].join('') + ']+$')).test(str)],
451    [['-format','-f'], commands.OPTION_LIST,null, [[f,manager.format[f]] for (f in manager.format)]],
452    [['-count','-c'], commands.OPTION_NOARG],
453    [['-browser','-b'],commands.OPTION_NORARG]
454];
455commands.addUserCommand(['viewSBMComments'], 'SBM Comments Viewer', //{{{
456    function(arg){ //{{{
457        var types =  liberator.globalVariables.def_sbms || 'hdlz';
458        var format = (liberator.globalVariables.def_sbm_format || 'id,timestamp,tags,comment').split(',');
459        var countOnly = false, openToBrowser = false;
460        var url = buffer.URL;
461        for (let opt in arg){
462            switch(opt){
463                case '-count':
464                    countOnly = true;
465                    break;
466                case '-browser':
467                    openToBrowser = true;
468                    break;
469                case '-type':
470                    if (arg[opt]) types = arg[opt];
471                    break;
472                case '-format':
473                    if (arg[opt]) format = arg[opt];
474                    break;
475                case "arguments":
476                    if (arg[opt].length > 0) url = arg[opt][0];
477                    break;
478            }
479        }
480
481        for (let i=0; i<types.length; i++){
482            let type = types.charAt(i);
483            if ( manager.type[type] ){
484                if ( cacheManager.isAvailable(url, type) ){
485                    liberator.log('cache avairable');
486                    if (openToBrowser)
487                        // TODO
488                        manager.open(cacheManager.get(url,type).toHTML(format,false), liberator.forceNewTab);
489                    else
490                        liberator.echo(cacheManager.get(url, type).toHTML(format,countOnly), true);
491                } else {
492                    try {
493                        openSBM(url, type, format, countOnly, openToBrowser);
494                    } catch(e){
495                        liberator.log(e);
496                    }
497                }
498            }
499        }
500    }, //}}}
501    {
502        argCount:"*",
503        options: options,
504        completer: function(context) completion.url(context, 'l')
505    }
506); //}}}
507
508/**
509 * cacheManager {{{
510 */
511var cacheManager = (function(){
512    var cache = {};
513    //             min  sec   millisec
514    var threshold = 30 * 60 * 1000;
515    var interval  = 10 * 60 * 1000;
516    var c_manager = {
517        get raw(){
518            return cache;
519        },
520        has: function(url){
521            if (cache[url])
522                return true;
523            else
524                return false;
525        },
526        add: function(sbmComments, url, type){
527            if (!cache[url]) cache[url] = {};
528            cache[url][type] = [new Date(), sbmComments];
529        },
530        get: function(url, type){
531            return cache[url][type][1];
532        },
533        delete: function(url, type) {
534            if (!cache[url]) return true;
535            if (!type) return delete cache[url];
536            return delete cache[url][type];
537        },
538        garbage: function(){
539            var date = new Date();
540            for (let url in cache){
541                for (let type in cache[url]){
542                    if (date - cache[url][type][0] > threshold) delete cache[url][type];
543                }
544            }
545        },
546        isAvailable: function(url, type){
547            if (cache[url] && cache[url][type] && new Date() - cache[url][type][0] < threshold)
548                return true;
549
550            return false;
551        }
552    };
553    return c_manager;
554})();
555//}}}
556
557return manager;
558})();
559// vim: sw=4 ts=4 sts=0 et fdm=marker:
Note: See TracBrowser for help on using the browser.