// ==UserScript== // @name Expand replies tree // @namespace http://lowreal.net/ // @include http://h.hatena.ne.jp/* // @include http://h.hatena.com/* // ==/UserScript== location.href = "javascript:"+encodeURIComponent(uneval(function () { (function (Global) { with (D()) { const WAIT = 1; const LEVEL = 3; const ICON = [ "data:image/png;base64,", "iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAD1BMVEX45u/y", "zt////+za4X///8WHaQkAAAABXRSTlP/////APu2DlMAAAAZdEVYdFNvZnR3", "YXJlAEFkb2JlIEltYWdlUmVhZHlxyWU8AAAARElEQVQI103OORIAQAgCQWD4", "/5s3cC+yLgtV5aVVyckmkW0PG8YBNK3IDj0k+vlNX3fzbkb8d++q+WrYVLZd", "1JaeDF8WzSMCBiKqHq0AAAAASUVORK5CYII=" ].join(""); addStyle([ "div.info .expand a {", "background: transparent url('",ICON,"') no-repeat scroll 0 65%;", "padding: 6px 0 2px 16px;", "margin: 0 0 0 3px", "}", ]); function applyJavaScript (entry) { entry = entry.parentNode.removeChild(entry); // remove event handlers entry.innerHTML = entry.innerHTML; // append to temporary element var t = h(); t.appendChild(entry); // remove duplicated star elements $X("../*/div/*[@class='title']/span[@class='hatena-star-comment-container' or @class='hatena-star-star-container']", entry).forEach(function (e) { e.parentNode.removeChild(e); }); // remove expand links $X("./div/div/span[@class='expand']", entry).forEach(function (i) { i.parentNode.removeChild(i); }); // remove script added links $X("./div/div/span[@class='delete']/img", entry).forEach(function (i) { i.parentNode.removeChild(i); }); Global.Hatena.Haiku.Pager.dispatchEvent('loadedEntries', entry.parentNode.wrappedJSObject || entry.parentNode); return entry; } function expandReplies (entry, level) { if (typeof level == "undefined") level = LEVEL; log("level:" + level); if (entry._expanded) return next(); if (level < 1) { // 深すぎるので、さらに展開するためのリンクをつけたうえで終了する。 return next(function (i) { log([entry, "too deep"]); if (!$X("boolean( ./div/div/a[.//img[@alt='Reply to'] and not(@class = '__loaded')] | ./div/div/span[@class='replies']/a[not(@class = '__loaded')] )", entry)) return; entry._gm_expandreplies_applied = false; addExpandLink(entry, function () { log("expand more -> child"); return expandReplies(entry, LEVEL); }); }).error(function (e) { alert(e) }); } entry._expanded = true; return parallel([ expandChildReplies(entry, level), expandParent(entry, level) ]); function expandParent (entry, level) { log(["expandParent", entry, level]); var a = $X("./div[@class='list-body']/div/a[img[@alt='Reply to']]", entry)[0]; var raw_href = $X("string(@href)", a); var es = $X("//div[@class='entry' and ./div/div/span[@class='timestamp']/a[@href = '"+raw_href+"']]"); if (a) a.style.opacity = "0.6"; return !a ? next() : es.length ? appendAndLoadNext(es[0]) : http.get(a.href).next(function (data) { var t = h(data.responseText.replace(/<\/?(?:script|i?frame)[^>]*>/g, "")); return parallel($X(".//div[@class='entries']/div[@class='entry' and not(@class='ad')]", t).map(appendAndLoadNext)); }); function appendAndLoadNext (e) { if (a) { a.style.opacity = "0.2"; a.className = "__loaded"; } if (e._expanded) return next(); e = applyJavaScript(e); var body = $X("./div[@class='list-body']", e)[0]; entry.parentNode.insertBefore(e, entry); body.appendChild(entry); $X("./div[@class='list-body']/div[@class='info']/span[@class='replies']/a[@href]", e).forEach(function (a) { // 既にロードされているなら半透明にしとく var raw_href = $X("string(@href)", a); if ($X("boolean(.//span[@class='timestamp']/a[@href = '"+raw_href+"'])", e)) { a.style.opacity = "0.2"; a.className = "__loaded"; } }); return wait(WAIT).next(function () { return call(expandReplies, e, level - 1); }); } } function expandChildReplies (entry, level) { log(["expandChildReplies", entry, level]); var body = $X("./div[@class='list-body']", entry)[0]; return parallel( $X("./div[@class='info']/span[@class='replies']/a[@href]", body).map(function (a) { var raw_href = $X("string(@href)", a); // don't show if this was showed entry if (a.className == "__loaded") return next(); a.style.opacity = "0.6"; // don't load already showed entry var es = $X("//div[@class='entry' and .//span[@class='timestamp']/a[@href = '"+raw_href+"']]"); if (es.length) return appendAndLoadNext(es[0]); return http.get(a.href).next(function (data) { // remove unnecessary tags var t = h(data.responseText.replace(/<\/?(?:script|i?frame)[^>]*>/g, "")); return parallel($X(".//div[@class='entries']/div[@class='entry' and not(@class='ad')]", t).map(appendAndLoadNext)); }); function appendAndLoadNext (e) { a.style.opacity = "0.2"; a.className = "__loaded"; var parentLink = $X("./div[@class='list-body']/div/a[img[@alt='Reply to']]", e)[0] parentLink.style.opacity = "0.2"; parentLink.className = "__loaded"; // 時系列に並ぶ様に注意する var time = $X("string(div/div/span[@class='timestamp']/a)", e); var ref = $X("./div[@class='entry' and not(@class='ad')]", body).filter(function (i) { return $X("string(div/div/span[@class='timestamp']/a)", i) < time; })[0]; body.insertBefore(applyJavaScript(e), ref); return wait(WAIT).next(function () { return call(expandReplies, e, level - 1); }); } }) ); } } function addExpandLink (e, handler) { if (e._gm_expandreplies_applied) return; e._gm_expandreplies_applied = true; var info = $X("./div/div[@class='info']", e)[0]; var link = dom("", info).root; link.addEventListener("click", function () { if (window.getSelection().toString()) return; if (e._loading) return; e._loading = true; log("Loading..."); link.innerHTML = "loading"; handler().next(function () { link.parentNode.removeChild(link); }).error(function (e) { alert(e) }); }, false); } // observe pagerize var num = 0; next(function () { var n = $X("count(//div[@class='entries']/div[@class='entry' and not(@class='ad')])"); if (num < n) { var entries = $X("//div[@class='entries']/div[@class='entry' and not(@class='ad')][.//img[@alt='Reply to'] or .//span[@class='replies']/a]"); loop(entries.length, function (n) { var e = entries[n].wrappedJSObject || entries[n]; addExpandLink(e, function () { return expandReplies(e). next(function () { if (log.element) { log.element.parentNode.removeChild(log.element); log.element = null; } }). error(function (e) { alert("Error"+e); }); }); }); num = n; } return wait(WAIT).next(arguments.callee); }). error(function (e) { log(e); }); } // end with /* template functions */ function h (s) { var d = document.createElement("div"); d.innerHTML = s; return d; } function addStyle (a) { dom("", document.body, { css : a.join("") }); } function dom (str, cur, txt) { var t, ret = {}, stack = [cur = cur || document.createElement("div")]; while (str.length) { if (str.indexOf("<") == 0) { if (t = str.match(/^\s*<(\/?[^\s>\/]+)([^>]+?)?(\/)?>/)) { var tag = t[1], attrs = t[2], isempty = !!t[3]; if (tag.indexOf("/") == -1) { child = document.createElement(tag); if (attrs) attrs.replace(/([a-z]+)=(?:'([^']+)'|"([^"]+)")/gi, function (m, name, v1, v2) { var v = text(v1 || v2); if (name == "class") ret[v] = child; child.setAttribute(name, v); } ); cur.appendChild(ret.root ? child : (ret.root = child)); if (!isempty) { stack.push(cur); cur = child; } } else cur = stack.pop(); } else throw("Parse Error: " + str); } else { if (t = str.match(/^([^<]+)/)) cur.appendChild(document.createTextNode(text(t[0]))); } str = str.substring(t[0].length); } return ret; function text (str) { return str .replace(/&(#(x)?)?([^;]+);/g, function (_, isNumRef, isHex, ref) { return isNumRef ? String.fromCharCode(parseInt(ref, isHex ? 16 : 10)): {"lt":"<","gt":"<","amp":"&"}[ref]; }) .replace(/#\{([^}]+)\}/, function (_, name) { return txt[name]; }); } } function log (m) { if (Global.console) Global.console.log(m); // if (!arguments.callee.element) { // arguments.callee.element = h("
").firstChild; // document.body.appendChild(arguments.callee.element); // } // arguments.callee.element.innerHTML = escapeHTML(m); } function escapeHTML (str) { str = String(str); var map = { "&" : "&", "<" : "<", ">" : ">"}; return str.replace(/[&<>]/g, function (m) { return map[m]; }); }; // extend version of $X // $X(exp); // $X(exp, context); // $X(exp, type); // $X(exp, context, type); function $X (exp, context, type /* want type */) { // @class='' を展開する exp = exp.replace(/@class\s*=\s*(?:"([^"]+)"|'([^']+)')/g, "contains(concat(' ', @class, ' '), ' $1$2 ')"); if (typeof context == "function") { type = context; context = null; } if (!context) context = document; exp = (context.ownerDocument || context).createExpression(exp, function (prefix) { return document.createNSResolver((context.ownerDocument == null ? context : context.ownerDocument).documentElement) .lookupNamespaceURI(prefix) || ({ "atom" : "http://purl.org/atom/ns#", "hatena" : "http://www.hatena.ne.jp/info/xmlns#" })[prefix] || document.documentElement.namespaceURI; }); switch (type) { case String: return exp.evaluate( context, XPathResult.STRING_TYPE, null ).stringValue; case Number: return exp.evaluate( context, XPathResult.NUMBER_TYPE, null ).numberValue; case Boolean: return exp.evaluate( context, XPathResult.BOOLEAN_TYPE, null ).booleanValue; case Array: var result = exp.evaluate( context, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); var ret = []; for (var i = 0, len = result.snapshotLength; i < len; ret.push(result.snapshotItem(i++))); return ret; case undefined: var result = exp.evaluate(context, XPathResult.ANY_TYPE, null); switch (result.resultType) { case XPathResult.STRING_TYPE : return result.stringValue; case XPathResult.NUMBER_TYPE : return result.numberValue; case XPathResult.BOOLEAN_TYPE: return result.booleanValue; case XPathResult.UNORDERED_NODE_ITERATOR_TYPE: { // not ensure the order. var ret = []; var i = null; while (i = result.iterateNext()) { ret.push(i); } return ret; } } return null; default: throw(TypeError("$X: specified type is not valid type.")); } } // Usage:: with (D()) { your code } // JSDeferred 0.2.1 (c) Copyright (c) 2007 cho45 ( www.lowreal.net ) // See http://coderepos.org/share/wiki/JSDeferred function D () { function Deferred () { return (this instanceof Deferred) ? this.init(this) : new Deferred() } Deferred.prototype = { init : function () { this._next = null; this.callback = { ok: function (x) { return x }, ng: function (x) { throw x } }; return this; }, next : function (fun) { return this._post("ok", fun) }, error : function (fun) { return this._post("ng", fun) }, call : function (val) { return this._fire("ok", val) }, fail : function (err) { return 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.parallel = function (dl) { var ret = new Deferred(), values = {}, num = 0; for (var i in dl) if (dl.hasOwnProperty(i)) { (function (d, i) { d.next(function (v) { values[i] = v; if (--num <= 0) { if (dl instanceof Array) { values.length = dl.length; values = Array.prototype.slice.call(values, 0); } ret.call(values); } }).error(function (e) { ret.fail(e); }); num++; })(dl[i], i); } if (!num) Deferred.next(function () { ret.call() }); ret.canceller = function () { for (var i in dl) if (dl.hasOwnProperty(i)) { dl[i].cancel(); } }; return ret; }; Deferred.wait = function (n) { var d = new Deferred(), t = new Date(); var id = setTimeout(function () { clearTimeout(id); d.call((new Date).getTime() - t.getTime()); }, n * 1000); d.canceller = function () { try { clearTimeout(id) } catch (e) {} }; return d; }; 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; }; Deferred.call = function (f, args) { args = Array.prototype.slice.call(arguments); f = args.shift(); return Deferred.next(function () { return f.apply(this, args); }); }; Deferred.loop = function (n, fun) { var o = { begin : n.begin || 0, end : (typeof n.end == "number") ? n.end : n - 1, step : n.step || 1, last : false, prev : null }; var ret, step = o.step; return Deferred.next(function () { function _loop (i) { if (i <= o.end) { if ((i + step) > o.end) { o.last = true; o.step = o.end - i + 1; } o.prev = ret; ret = fun.call(this, i, o); if (ret instanceof Deferred) { return ret.next(function (r) { ret = r; return Deferred.call(_loop, i + step); }); } else { return Deferred.call(_loop, i + step); } } else { return ret; } } return (o.begin <= o.end) ? Deferred.call(_loop, o.begin) : null; }); }; Deferred.register = function (name, fun) { this.prototype[name] = function () { return this.next(Deferred.wrap(fun).apply(null, arguments)); }; }; Deferred.wrap = function (dfun) { return function () { var a = arguments; return function () { return dfun.apply(null, a); }; }; }; Deferred.register("loop", Deferred.loop); Deferred.register("wait", Deferred.wait); Deferred.define = function (obj, list) { if (!list) list = ["parallel", "wait", "next", "call", "loop"]; if (!obj) obj = (function () { return this })(); for (var i = 0; i < list.length; i++) { var n = list[i]; obj[n] = Deferred[n]; } return Deferred; }; function xhttp (opts) { var d = Deferred(); if (opts.onload) d = d.next(opts.onload); if (opts.onerror) d = d.error(opts.onerror); opts.onload = function (res) { d.call(res); }; opts.onerror = function (res) { d.fail(res); }; setTimeout(function () { GM_xmlhttpRequest(opts); }, 0); return d; } xhttp.get = function (url) { return xhttp({method:"get", url:url}) }; xhttp.post = function (url, data) { return xhttp({method:"post", url:url, data:data, headers:{"Content-Type":"application/x-www-form-urlencoded"}}) }; function http (opts) { var d = Deferred(); var req = new XMLHttpRequest(); req.open(opts.method, opts.url, true); 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; } http.get = function (url) { return http({method:"get", url:url}) }; http.post = function (url, data) { return http({method:"post", url:url, data:data, headers:{"Content-Type":"application/x-www-form-urlencoded"}}) }; http.jsonp = function (url, params) { if (!params) params = {}; var Global = (function () { return this })(); var d = Deferred(); var cbname = params["callback"]; if (!cbname) do { cbname = "callback" + String(Math.random()).slice(2); } while (typeof(Global[cbname]) != "undefined"); params["callback"] = cbname; url += (url.indexOf("?") == -1) ? "?" : "&"; for (var name in params) if (params.hasOwnProperty(name)) { url = url + encodeURIComponent(name) + "=" + encodeURIComponent(params[name]) + "&"; } var script = document.createElement('script'); script.type = "text/javascript"; script.charset = "utf-8"; script.src = url; document.body.appendChild(script); Global[cbname] = function callback (data) { delete Global[cbname]; document.body.removeChild(script); d.call(data); }; return d; }; Deferred.Deferred = Deferred; Deferred.http = http; Deferred.xhttp = xhttp; return Deferred; }// End of JSDeferred // {{{ // }}} })(this); }))+"()";