// ==UserScript==
// @name sscsort
// @namespace slatestarcodex
// @include http://slatestarcodex.com/20*
// @version 1
// @grant none
// ==/UserScript==
// What is this? Install as Greasemonkey-script to get a sort-comments bar:
// https://imgur.com/WlXqvLp
// Fast enough with ~100 comments, with 300+ it is quite slow.
// No warranty for usefulness, merchantability etc etc. I did not test
// this very much, might be that comments disappear or are scrambled.
// Feel free to modify, distribute, use inside your own project,
// do whatever you want with it. I place this code in the public domain :-)
var lastby = "recency"; // property that was last sorted by
// sort directions
var recencydirection = 1;
var bestdirection = 1;
var topdirection = 1;
var allComments = []; // all the list elements.
var isMenuScrolling = 0; // toggle 0/1 when scrolling below the menu & fixing it to the top
// create the menu
d = document.createElement('div');
d.innerHTML = '<div id="sortbar"><div id="sortbardiv" \
style="background-color:#FAFAFA;color:#333;padding:4px;margin-top:4px;border:1px solid;\
z-index:1000;width:548px;border-radius:5px;"> \
<span id="recencysorter" style="background-color:#DDD;padding:0px 3px;">Old ^</span> \
<span id="mostsorter" style="background-color:#DDD;padding:0px 3px;">Most </span> \
<span id="topsorter" style="background-color:#DDD;padding:0px 3px;">Top </span> \
<span id="lastlevelstable" style="background-color:#DDD;padding:0px 3px;">\
<input id="llstablecheck" type="checkbox" checked="checked" style="vertical-align:middle;">\
5th level always oldest first</input></span> \
</div></div>';
//insert the menu
coms = document.getElementById('comments');
coms.insertBefore(d, coms.childNodes[0]);
// the events when clicking the menu buttons:
document.getElementById('recencysorter').onclick = function(event) {
sortComments("recency");
document.getElementById('recencysorter').childNodes[0].data = "Old " +
((recencydirection<0)?"v":"^");
document.getElementById('mostsorter').childNodes[0].data = "Most \u00a0\u00a0";
document.getElementById('topsorter').childNodes[0].data = "top \u00a0\u00a0";
};
document.getElementById('mostsorter').onclick = function(event) {
sortComments("best");
document.getElementById('mostsorter').childNodes[0].data = "Most " +
((bestdirection<0)?"v":"^");
document.getElementById('recencysorter').childNodes[0].data = "Old \u00a0\u00a0";
document.getElementById('topsorter').childNodes[0].data = "top \u00a0\u00a0";
};
document.getElementById('topsorter').onclick = function(event) {
sortComments("top");
document.getElementById('topsorter').childNodes[0].data = "Top " +
((topdirection<0)?"v":"^");
document.getElementById('mostsorter').childNodes[0].data = "Most \u00a0\u00a0";
document.getElementById('recencysorter').childNodes[0].data = "Old \u00a0\u00a0";
};
document.getElementById('llstablecheck').onchange = function(event) {
// sort again when checkbox state changes.
if (lastby == "recency" || lastby == "best") {
if (recencydirection == 1) {
// if we sorted oldest-first before, we don't need to resort.
// this is also the case if we sorted best-first, since oldest-first is
// the tie-breaker.
return;
}
} else if (topdirection == -1) {
// if we sorted top first, it puts more recent comments to the top; only when we
// had top-last we don't need to resort.
return;
}
//
var lastsort = lastby;
lastby = ""; // if we dont reset this, the sort direction will reverse.
sortComments(lastsort);
};
// make sure the menu gets fixed to the top when scrolling below it
scrollevent = function(event) {
var posn = document.getElementById('sortbar').getBoundingClientRect();
if (posn.top < 0) {
if (!isMenuScrolling) {
document.getElementById('sortbar').style.height=(posn.bottom -
posn.top) + "px";
document.getElementById('sortbardiv').style.position=
"fixed";
document.getElementById('sortbardiv').style.top="0px";
isMenuScrolling = 1;
}
} else {
if (isMenuScrolling) {
document.getElementById('sortbardiv').style.position="relative";
isMenuScrolling = 0;
}
}
};
window.onscroll = scrollevent;
scrollevent(0); // trigger event if we start below the menu.
// save score values of comments
function tagComments() {
commentol = document.querySelectorAll('#comments > .commentlist')[0];
tagScores(commentol);
allComments.sort(function(a, b) {
return (parseInt(a.id.match(/\d+/)[0], 10) -
parseInt(b.id.match(/\d+/)[0], 10));
});
j = 0;
for(var i = allComments.length; i--;) {
allComments[i].setAttribute("data-comments-after", j++);
}
tagPi(commentol);
}
function tagScores(list) {
// recursively tag each li with score values
// data-score: total number of comments that are anchestors of a li
// data-escore: number of comments that are anchestors, weighted by 2^(-l), where
// l is the distance to this comment. Direct children: l=0. Grandchild: l=1 etc.
var totalscore = 0;
var totalexpscore = 0;
for (var i = list.childNodes.length; i--;) {
currentchild = list.childNodes[i];
if (currentchild.className=="post pingback") { // skip pingbacks
continue;
}
if (currentchild.nodeName == "LI") {
tscores = [0, 0];
// find ul child
for (var j = currentchild.childNodes.length; j--;) {
if (currentchild.childNodes[j].nodeName == "UL") {
tscores = tagScores(currentchild.childNodes[j]);
break;
}
}
currentchild = list.childNodes[i];
currentchild.setAttribute("data-score", tscores[0]);
currentchild.setAttribute("data-escore", tscores[1]);
// save the id as a number string, so we can quickly parse it when sorting.
currentchild.setAttribute("data-id", currentchild.id.match(/\d+/)[0]);
totalscore += tscores[0] + 1;
totalexpscore += tscores[1] / 2 + 1;
allComments.push(list.childNodes[i]);
}
}
return [totalscore, totalexpscore];
}
function tagPi(list) {
// recursively tag each li with pi values
// data-pi: average proportion of comments that are sub-comments.
var numerator = 1;
var denominator = 2;
for (var i = list.childNodes.length; i--;) {
currentchild = list.childNodes[i];
if (currentchild.className=="post pingback") { // skip pingbacks
continue;
}
if (currentchild.nodeName == "LI") {
// find ul child
for (var j = currentchild.childNodes.length; j--;) {
if (currentchild.childNodes[j].nodeName == "UL") {
tagPi(currentchild.childNodes[j]);
break;
}
}
currentchild = list.childNodes[i];
numerator += parseInt(currentchild.getAttribute("data-score"), 10);
denominator += parseInt(currentchild.getAttribute("data-comments-after"), 10);
}
}
for (var i2 = list.childNodes.length; i2--;) {
currentchild = list.childNodes[i2];
if (currentchild.className=="post pingback") { // skip pingbacks
continue;
}
if (currentchild.nodeName == "LI") {
currentchild.setAttribute("data-pi", 1.0 * numerator / denominator);
}
}
}
function sortComments(by) {
commentol = document.querySelectorAll('#comments > .commentlist')[0];
commentol.style.display = "none"; // faster DOM manipulation if hidden.
if (by == lastby) {
if (lastby == "recency") {
recencydirection *= -1;
} else if (lastby == "best") {
bestdirection *= -1;
} else if (lastby == "top") {
topdirection *= -1;
}
}
sortLIs(commentol, by);
lastby = by;
//need to get the OL anew, since we replaced it in sortLIs.
document.querySelectorAll('#comments > .commentlist')[0].style.display = "block";
}
// the sorting functions. Return +ve if a belongs after b,
// -ve if b belongs after a, and 0 in a tie.
recencysorter = function(a, b) {
// sort by comment ID number; we assume it is monotonically ascending.
return (parseInt(a.getAttribute("data-id"), 10) -
parseInt(b.getAttribute("data-id"), 10));
};
bestsorter = function(a, b) {
// sort by number of sub-comments; break ties with most number of direct children.
diff = parseInt(b.getAttribute("data-score"), 10) -
parseInt(a.getAttribute("data-score"), 10);
if (diff === 0) {
// break ties with 'escore':
// more weight on lower level comments.
diff = parseInt(b.getAttribute("data-escore"), 10) -
parseInt(a.getAttribute("data-escore"), 10);
}
return diff;
};
topsorter = function(a, b) {
// sort by the ratio:
// (number of children + 1) / (number of comments that were made after this one + 2)
// the '+1' prevents the most recent comment from always winning,
// and childless comments from always losing (and has some handwavy justification from bayesian stats.)
// If there are lots of comments, this gives an advantage to very recent comments when
// the number of comments is high. Is this sensible?
// One could say that the very recent comments are 'probably good', since someone did not find
// another thread to post it (although there are already lots of threads). Also, if the comment
// does not gather replies, it very rapidly disappears into averageness. I am not 100% convinced, however.
// Maybe a good thing would be to fit a Beta-distribution to the number of comments.
alpha = 2 * parseFloat(b.getAttribute("data-pi")); // the '2' is relatively arbitrary here...
bca = parseInt(b.getAttribute("data-comments-after"), 10) + 2;
bscore = parseInt(b.getAttribute("data-score"), 10) + alpha;
aca = parseInt(a.getAttribute("data-comments-after"), 10) + 2;
ascore = parseInt(a.getAttribute("data-score"), 10) + alpha;
// compare bscore / bca with ascore / aca.
diff = bscore * aca - ascore * bca;
return diff;
};
function sortLIs(list, by, iii) {
// recursively sort comment list
// iii is for debugging purposes: array of index of list element currently looked at.
// however. we also need iii to remember which level we are (and sort the 5th level
// differently).
if(typeof(iii)==='undefined') {iii = [];}
iii.push(0);
var new_list = list.cloneNode(false);
var larr = [];
var pingbacks = [];
// Add all li-elements to an array
for(var i = list.childNodes.length; i--;){
currentchild = list.childNodes[i];
if(currentchild.nodeName === 'LI') { //check for 'li', we want to skip empty textNodes.
if (currentchild.className=="post pingback") { // skip pingbacks
pingbacks.push(currentchild);
continue;
}
for (var j = currentchild.childNodes.length; j--;) {
// look for child comments: they would be in a UL-element.
if (currentchild.childNodes[j].nodeName == "UL") {
iii[iii.length-1]++;
sortLIs(currentchild.childNodes[j], by, iii);
break; // only one UL per comment.
}
}
larr.push(list.childNodes[i]); // at this point currentchild has been burned.
}
}
iii.pop();
if (document.getElementById('llstablecheck').checked && iii.length >= 4) {
// if we are in the last level and want it always
larr.sort(recencysorter);
} else if (by == "recency") {
// sort oldest first / last
larr.sort(function(a, b) {
return recencysorter(a, b) * recencydirection;
});
} else if (by == "best") {
// sort by number of descendant comments
larr.sort(function(a, b) {
diff = bestsorter(a, b) * bestdirection;
if (diff === 0) {
// tie breaker is recency, with default recencydirection
diff = recencysorter(a, b) * recencydirection;
}
return diff;
});
} else if (by == "top") {
larr.sort(function(a, b) {
diff = topsorter(a, b);
if (diff === 0) {
// first tie breaker is total score, with sort direction topdirection.
diff = bestsorter(a, b);
} if (diff === 0) {
// second tie breaker is recency, with default recencydirection
diff = recencysorter(a, b) * recencydirection;
} else {
// if we don't use recency, sort direction is applied.
diff *= topdirection;
}
return diff;
});
}
// Add the pingbacks at the end.
larr = larr.concat(pingbacks);
for(var i2 = 0; i2 < larr.length; i2++) {
new_list.appendChild(larr[i2]);
}
list.parentNode.replaceChild(new_list, list);
}
tagComments();