Skip to content

Instantly share code, notes, and snippets.

@wayneburkett
Last active September 26, 2015 07:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wayneburkett/1061565 to your computer and use it in GitHub Desktop.
Save wayneburkett/1061565 to your computer and use it in GitHub Desktop.
Sort the Hacker News front page
// hnsort
// v0.3
// Copyright (c) 2009, Wayne Burkett
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html
// ==UserScript==
// @name hnsort
// @namespace http://wayneburkett.com
// @description Sort articles on the Hacker News homepage
// @match https://news.ycombinator.com/show*
// @match https://news.ycombinator.com/new*
// @match https://news.ycombinator.com/
// ==/UserScript==
function $(id) {
return document.getElementById(id);
}
// returns an array of "story" meta-objects
function getStories(tbody) {
if (getStories._vals)
return getStories._vals;
var stories = document.evaluate("id('articlesTable')//tr[td[@class='title']]", tbody, null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
var triplets = [];
var length = stories.snapshotLength;
for (var i = 0; i < length - 1; i++) {
var headline = stories.snapshotItem(i);
var subtext = headline.nextElementSibling.getElementsByClassName("subtext")[0];
if (subtext) {
addSubtextHooks(subtext);
triplets.push(createMetaObj(headline, i, length));
}
}
getStories._vals = triplets;
return getStories._vals;
}
// creates a meta-object with details for the given
// headline; used later to determine sort order
function createMetaObj(headline, pos, length) {
var subtext = headline.nextElementSibling;
var res = subtext.textContent.split(/\s+/);
var isJobPosting = (res.length === 5);
var agePos = isJobPosting ? 1 : 5;
var commentsPos = (res[9] === "flag") ? 11 : 9;
return {
elements: [headline, subtext, subtext.nextElementSibling],
rank: length - pos,
points: !isJobPosting ? parseInt(res[1], 10) : 0,
comments: !isJobPosting ? (parseInt(res[commentsPos], 10) || 0) : -1,
age: (new Date()).getTime() -
(parseInt(res[agePos], 10) * getMultiplier(res[agePos + 1]))
}
}
function getMultiplier(unit) {
var mult = 1;
switch (unit) {
case "day":
case "days":
mult *= 24;
case "hour":
case "hours":
mult *= 60;
case "minute":
case "minutes":
mult *= 60;
case "second":
case "seconds":
mult *= 1000;
}
return mult;
}
// wraps the "n hours ago" text in a span and gives it a class
// name so that it's easier to select later
function addSubtextHooks(subtext) {
// the timestamp is now a link on regular posts, but not on job posts, for
// some reason, so we can identify a regular post by the presence of a
// timestamp link
var timeLink = subtext.getElementsByTagName("a")[1];
var target = timeLink || subtext
target.classList.add("age");
}
// wraps the given text node with a <span>
function wrapTextNode(node, clazz) {
var el = document.createElement("span");
var clone = node.cloneNode(true);
el.className = clazz;
el.appendChild(clone);
node.parentNode.replaceChild(el, node);
el.replaceChild(node, clone);
return clone;
}
// splits a text node in two at the specified offset
// and returns a reference to the first half (which
// is the original node)
function splitTextNode(node, offset) {
var val = node.nodeValue;
var txt = document.createTextNode(val.substring(offset, val.length));
node.nodeValue = val.substring(0, offset);
node.parentNode.insertBefore(txt, node.nextSibling);
return node;
}
// marks the given sort link as selected and unmarks its siblings
function updateSelected(link) {
var sibs = link.parentNode.childNodes;
for (var i = 0; i < sibs.length; i++) {
if (sibs[i].nodeType != sibs[i].ELEMENT_NODE)
continue;
sibs[i].style.fontWeight = (sibs[i] == link) ? "bold" : "normal";
}
}
// generates an array of CSS rules
function genStyles(selectors, name) {
var rules = [];
for (var sel in selectors) {
rules.push(selectors[sel] + "{color:" +
(sel === name ? "red" : "inherit") + " !important;}")
}
return rules.join("\n");
}
// applies latest styles (based on which sort link is selected)
function updateStyles(selected) {
var selectors = {
age: "#articlesTable td.subtext .age",
points: "#articlesTable .subtext .score",
comments: "#articlesTable .subtext a:last-child"
};
var styles = genStyles(selectors, selected);
$("hnsort").innerHTML = styles;
}
function createSortLink(text, sortKey) {
var link = document.createElement("a");
link.appendChild(document.createTextNode(text));
link.href = "#";
link.addEventListener("click", function(e) {
var tbody = $("articlesTable").firstElementChild;
sort(tbody, (sortKey || text), (link.style.fontWeight == "bold"));
updateSelected(link);
updateStyles(text);
return false;
}, false);
return link;
}
function sort(tbody, sortKey, sorted) {
var refEl = tbody.lastElementChild.previousElementSibling;
(function(stories) {
return sorted ? stories.reverse() :
stories.sort(function(a, b) {
return b[sortKey] - a[sortKey]
});
})(getStories(tbody)).forEach(function(el) {
el.elements.forEach(function(item) {
tbody.insertBefore(item, refEl)
});
});
}
// adds an empty style element to the head of the document
// that can be later retrieved by ID and updated
function addStyleElement() {
var head = document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
style.id = "hnsort";
head.appendChild(style);
}
function getArticlesTable() {
// the first table wraps everything; the second table is the header bar;
// the third table contains the articles (there's no great way to target
// most elements on hn)
return document.getElementsByTagName("table")[2];
}
// the first hn spacer row
function getEmptyRow() {
// see earlier comment about selecting elements on hn
return document.querySelector("tr[style='height:10px']");
}
function insertControls() {
var emptyRow = getEmptyRow();
var articlesTable = getArticlesTable();
var linksWrapper = document.createElement("td");
linksWrapper.id = "sortLinks";
linksWrapper.className = "title";
articlesTable.id = "articlesTable";
var links = [
createSortLink("#", "rank"),
createSortLink("points"),
createSortLink("age"),
createSortLink("comments")];
links[0].style.fontWeight = "bold";
linksWrapper.appendChild(document.createTextNode("\u00a0\u00a0Sort by "));
for (var i = 0; i < links.length; i++) {
if (i != 0)
linksWrapper.appendChild(document.createTextNode(" | "));
linksWrapper.appendChild(links[i]);
}
// we're using the empty row as a pivot point and duplicating its
// hacky spacing behavior
emptyRow.parentNode.insertBefore(emptyRow.cloneNode(true), emptyRow);
emptyRow.parentNode.insertBefore(emptyRow.cloneNode(true), emptyRow.nextElementSibling);
emptyRow.appendChild(linksWrapper);
}
insertControls();
addStyleElement();
// 2015-04-18 - 0.5 - Handle changes to site layout
// 2014-07-06 - 0.4 - Enable on show and shownew
// 2011-06-30 - 0.3 - Chrome compat
// fixed bug that prevented sorting by comments when logged in with a "flag" link
// black bar of doom compat
// handles job postings (only sortable by age)
// works on the "new" page
// 2009-08-12 - 0.2 - now plays nicely with the "Hacker News Toolkit"
// 2009-08-12 - 0.1 - released
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment