// ==UserScript==
// @name Google Scholar Monitor
// @namespace http://scholar.dforge.cse.ucsc.edu/
// @description Adds Google scholar monitors inspired by http://persistent.info/archives/2005/03/01/gmail-searches.
// @include http://scholar.google.com/
// @include http://scholar.google.com/scholar?*
// ==/UserScript==
// Google Scholar Monitor
// Original Author: Mihai Parparita <mihai@persistent.info>
// Author: Sung Kim <hunkim@cs.ucsc.edu>
// License: a Creative Commons, http://creativecommons.org/licenses/by-sa/2.0/
// $Rev: 28 $
// $LastChangedDate: 2005-05-06 07:55:27 -0700 (Fri, 06 May 2005) $
(function() {
// Utility functions
function getObjectMethodClosure(object, method) {
return function() {
return object[method]();
}
}
function getObjectMethodClosure1(object, method) {
return function(arg) {
return object[method](arg);
}
}
function getObjectMethodClosure2(object, method) {
return function(arg) {
return object[method](arg)[0];
}
}
function getDateString(date) {
return date.getFullYear() + "/" +
(date.getMonth() + 1) + "/" +
date.getDate();
}
function getCookie(name) {
var re = new RegExp(name + "=([^;]+)");
var value = re.exec(document.cookie);
return (value != null) ? unescape(value[1]) : null;
}
function setCookie(name, value) {
var today = new Date();
// plus 28 years
var expiry = new Date(today.getTime() + 28 * 365.24 * 24 * 60 * 60 * 1000);
document.cookie = name + "=" + escape(value) +
"; expires=" + expiry.toGMTString() +
"; path=/";
}
// Shorthand
var newNode = getObjectMethodClosure1(document, "createElement");
var newText = getObjectMethodClosure1(document, "createTextNode");
var getNode = getObjectMethodClosure1(document, "getElementById");
var getTag = getObjectMethodClosure1(document, "getElementsByTagName");
// Contants
const MONOSPACE_RULE = ".mb {font-family: monospace !important;}";
const NORMAL_RULE = ".mb {}";
const MESSAGE_BODY_FONT_RULE_INDEX = 0;
const RULES = new Array(
// Block in sidebar
".searchesBlock {-moz-border-radius: 5px; background: #dcf6db; margin: 10px 7px 0 0; padding: 3px;}",
".refreshButton {display: block; cursor: pointer; float: right; margin-top: -2px;}",
".searchesBlockList {background: white;}",
".listItem {color: #111199; font-size: 90%; text-decoration: underline; cursor: pointer}",
".editLink {text-align: right; color: #ca5c22; padding: 2px 5px 5px 0; font-size: 80%; text-decoration: underline; cursor: pointer}",
// Edit page
".searchesContainer {-moz-border-radius: 10px; background: #fad163; padding: 10px;}",
".innerContainer {background: #fff7d7; text-align: center; padding: 10px;}",
".searchesList {width: 100%;}",
".searchesList th {text-align: left; font-size: 90%;}",
".searchesList td {padding: 10px 0 10px 0; vertical-align: bottom;}",
".searchesList td.divider {background: #fad163; height: 3px; padding: 0;}",
".editItem {font-size: 80%; }",
".labelCell {width: 210px;}",
".labelCell input {width: 200px;}",
".cancelButton {margin-right: 5px;}",
".editCell {}",
".editCell input {width: 100%}",
".saveButton {margin-left: 5px; font-weight: bold;}"
);
const TOGGLE_FONT_IMAGE = "data:image/gif;base64,R0lGODlhEAAQAIABAAAAzP%2F%2" +
"F%2FyH5BAEAAAEALAAAAAAQABAAAAImjI%2BJwO28wIGG1rjUlFrZvoHJVz0SGXBqymXphU5" +
"Y17Kg%2BnixKBYAOw%3D%3D";
const REFRESH_IMAGE = "data:image/gif;base64,R0lGODlhDQAPANU5AM%2BtUs6sUunDX" +
"PfPYt65WK%2BTRaiMQvXNYfDJX9m1VtSxVIBrM7GURsKiTZqBPeS%2FWo94OZmAPebBW6WK" +
"QbiaSdOwU35qMpV9O4t0N4NuNI12OIFsM9u3V7mbSaaLQtazVcyqUZ6EP%2BC7WX1oMbudS" +
"semT62QRPjPYuvFXXtmMbSXR%2BK9WohyNvLKYOfBXPPLYJB4Ob6fS5R8O%2B3GXqGGQK%2" +
"BSRauPROG9WW5cK%2F%2F%2F%2FwAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADkALAAAAAANA" +
"A8AAAZvwJxwSMzdiKcAg8YIDEyG4QPjABAUhgUuInQtAsQaDqcRwj7EUmY8yiUuReJtQInF" +
"h5JEAXQX3mwzD305FSRGBAN3Eys5HWM4LAdDIiFCCmMbAkMcMghCBDgpEAUNKg4eL0MoFgI" +
"tAA0AnkQHmoNBADs%3D";
const TRIANGLE_IMAGE = "data:image/gif;base64," +
"R0lGODlhCwALAPEAAP///wAAAA4ODv///yH5BAEAAAAALAAAAAALAAsAAAJFBIgQIECAAAEm" +
"BAgQIECECQECBAgwYUKAAAEiTJgQIECACRMmBAgQYcKEAAECVJgQIECACBMCBAgQYEKAAAEC" +
"RAgQIEAUADs=";
const OPEN_TRIANGLE_IMAGE = "data:image/gif;base64,"+
"R0lGODlhCwALAKEAAP///wAAAA4ODv///yH5BAEAAAAALAAAAAALAAsAAAIShI+pm+HiogOy" +
"njqRVZFRDzIFADs="
const RESULT_SIZE_RE = new RegExp("Results\\D+(\\d+)\\D+(\\d+)\\D+([0-9,]+)");
const DEFAULT_SEARCHES = {
"author:whitehead WebDAV": "WebDAV",
"Software Engineering year:2005": "Software Engineering 2005",
"author:mccarthy artificial intelligence": "AI Mccarthy"
};
const SEARCHES_COOKIE = "ScholorPersistentSearches";
const SEARCHES_TOGGOLE_COOKIE = "ScholorPersistentSearchesToggle";
const ONE_DAY = 24 * 60 * 60 * 1000;
const ONE_HOUR = 60 * 60 * 1000;
// Globals
var mbStyleSheet = null;
var currentRule = NORMAL_RULE;
// To check previous items
var oldSearches;
var searches = new Array();
var searchesBlock = null;
var searchesBlockHeader = null;
var searchesBlockList = null;
var editLink = null;
var hiddenNodes = null;
var searchesContainer = null;
var searchesList = null;
function initializePersistentSearches() {
// FIXME: Do we need any other condition to check?
searchesBlock = newNode("div");
searchesBlock.id = "nb_1";
searchesBlock.className = "searchesBlock";
// header
searchesBlockHeader = newNode("div");
searchesBlockHeader.className = "s h";
searchesBlock.appendChild(searchesBlockHeader);
var refreshButton = newNode("img");
refreshButton.src = REFRESH_IMAGE;
refreshButton.className = "refreshButton";
refreshButton.height = 13;
refreshButton.height = 15;
refreshButton.onclick = refreshPersistentSearches;
searchesBlockHeader.appendChild(refreshButton);
searchesBlockHeader.triangleImage = newNode("img");
searchesBlockHeader.triangleImage.src = OPEN_TRIANGLE_IMAGE;
searchesBlockHeader.triangleImage.width = 11;
searchesBlockHeader.triangleImage.height = 11;
searchesBlockHeader.triangleImage.onclick = togglePersistentSearches;
searchesBlockHeader.appendChild(searchesBlockHeader.triangleImage);
var searchesText = newNode("span");
searchesText.appendChild(newText(" Scholar Monitor "));
searchesText.onclick = togglePersistentSearches;
searchesBlockHeader.appendChild(searchesText);
// searches list
searchesBlockList = newNode("div");
searchesBlockList.className = "searchesBlockList";
searchesBlock.appendChild(searchesBlockList);
editLink = newNode("div");
editLink.appendChild(newText("Edit monitor items"));
editLink.className = "lk cs editLink";
editLink.onclick = editPersistentSearches;
searchesBlockList.appendChild(editLink);
// Check if the box is closed
var toggleStatus = getCookie(SEARCHES_TOGGOLE_COOKIE);
if (toggleStatus=="closed") {
searchesBlockList.style.display = "none";
searchesBlockHeader.triangleImage.src = TRIANGLE_IMAGE;
}
// Check Moniroting Item
if (getCookie(SEARCHES_COOKIE)) {
restorePersistentSearches();
} else {
for (var query in DEFAULT_SEARCHES) {
addPersistentSearch(new PersistentSearch(query, DEFAULT_SEARCHES[query]));
}
}
// FIXME: Find the right place to add the monitor box?
var body = document.getElementsByTagName('table')[2];
// Not many table, probably the first page
if (!body) {
body = document.getElementsByTagName('table')[0];
}
body.appendChild(searchesBlock);
}
function refreshPersistentSearches() {
for (var i=0; i < searches.length; i++) {
searches[i].getResultSize(true);
}
return false;
}
function restorePersistentSearches() {
var serializedSearches = getCookie(SEARCHES_COOKIE).split("|");
for (var i=0; i < serializedSearches.length; i++) {
var search = PersistentSearch.prototype.fromString(serializedSearches[i]);
addPersistentSearch(search);
}
}
function savePersistentSearches() {
var serializedSearches = new Array();
for (var i=0; i < searches.length; i++) {
serializedSearches.push(searches[i].toString());
}
setCookie(SEARCHES_COOKIE, serializedSearches.join("|"));
}
function clearPersistentSearches() {
for (var i=0; i < searches.length; i++) {
var item = searches[i].getListItem();
if (item.parentNode) {
item.parentNode.removeChild(item);
}
}
oldSearches = searches;
searches = new Array();
}
function addPersistentSearch(search) {
searches.push(search);
searchesBlockList.insertBefore(search.getListItem(), editLink);
savePersistentSearches();
}
function editPersistentSearches(event) {
var container = getNode("nb_1");
hiddenNodes = new Array();
for (var i = container.firstChild; i; i = i.nextSibling) {
hiddenNodes.push(i);
i.style.display = "none";
}
searchesContainer = newNode("div");
searchesContainer.className = "searchesContainer";
searchesContainer.innerHTML += "<b>Scholar Monitor Items </b>";
container.appendChild(searchesContainer);
var innerContainer = newNode("div");
innerContainer.className = "innerContainer";
innerContainer.innerHTML +=
'<p>Use <a href="http://scholar.google.com/scholar/about.html" target="_blank">operators</a> ' +
'to specify queries. Delete an item\'s query to remove it.</p>';
searchesContainer.appendChild(innerContainer);
searchesList = newNode("table");
searchesList.className = "searchesList";
innerContainer.appendChild(searchesList);
var headerRow = newNode("tr");
searchesList.appendChild(headerRow);
headerRow.appendChild(newNode("th")).appendChild(newText("Label"));
headerRow.appendChild(newNode("th")).appendChild(newText("Query"));
for (var i=0; i < searches.length; i++) {
searchesList.appendChild(searches[i].getEditItem());
var dividerRow = newNode("tr");
var dividerCell = dividerRow.appendChild(newNode("td"));
dividerCell.className = "divider";
dividerCell.colSpan = 3;
searchesList.appendChild(dividerRow);
}
var newSearch = new PersistentSearch("", "");
var newItem = newSearch.getEditItem();
newItem.firstChild.innerHTML =
"<h4>Create a new monitor items:</h4>" +
newItem.firstChild.innerHTML;
searchesList.appendChild(newItem);
var cancelButton = newNode("button");
cancelButton.appendChild(newText("Cancel"));
cancelButton.className = "cancelButton";
cancelButton.onclick = cancelEditPersistentSearches;
innerContainer.appendChild(cancelButton);
var saveButton = newNode("button");
saveButton.appendChild(newText("Save Changes"));
saveButton.className = "saveButton";
saveButton.onclick = saveEditPersistentSeaches;
innerContainer.appendChild(saveButton);
// Make clicks outside the edit area hide it
getNode("nav").onclick = cancelEditPersistentSearches;
// Since we're in a child of the "nav" element, the above handler will get
// triggered immediately unless we stop this event from propagating
event.stopPropagation();
return false;
}
function cancelEditPersistentSearches() {
searchesContainer.parentNode.removeChild(searchesContainer);
searchesContainer = null;
for (var i=0; i < hiddenNodes.length; i++) {
hiddenNodes[i].style.display = "";
}
getNode("nav").onclick = null;
return true;
}
// Find old search item using label and query
function findOldSearches(label, query) {
for (var i=0; i < oldSearches.length; i++) {
if (oldSearches[i].label == label &&
oldSearches[i].query == query) {
return oldSearches[i];
}
}
return null;
}
// Save new items
// It resets all existing items
function saveEditPersistentSeaches() {
clearPersistentSearches();
for (var row = searchesList.firstChild; row; row = row.nextSibling) {
var cells = row.getElementsByTagName("td");
if (cells.length != 2) {
continue;
}
var label = cells[0].getElementsByTagName("input")[0].value;
var query = cells[1].getElementsByTagName("input")[0].value;
if (label && query) {
var search = findOldSearches(label, query);
// Not found
if (search==null) {
search = new PersistentSearch(query, label);
}
addPersistentSearch(search);
}
}
// cancelling just hides everything, which is what we want to do
cancelEditPersistentSearches();
}
function togglePersistentSearches() {
if (searchesBlockList.style.display == "none") {
searchesBlockList.style.display = "";
searchesBlockHeader.triangleImage.src = OPEN_TRIANGLE_IMAGE;
setCookie(SEARCHES_TOGGOLE_COOKIE, "opened");
} else {
searchesBlockList.style.display = "none";
searchesBlockHeader.triangleImage.src = TRIANGLE_IMAGE;
setCookie(SEARCHES_TOGGOLE_COOKIE, "closed");
}
return false;
}
function PersistentSearch(query, label) {
this.query = query;
this.label = label;
this.totalResults = -1;
this.oldResults = -1;
this.newSetDate = -1;
this.setDate = -1;
this.listItem = null;
this.editItem = null;
this.resultSizeItem = null;
}
// Used to save function or object to Cookie String
PersistentSearch.prototype.toString = function() {
var serialized = new Array();
for (var property in this) {
if (typeof(this[property]) != "function" &&
typeof(this[property]) != "object") {
serialized.push(property + "=" + this[property]);
}
}
return serialized.join("&");
}
PersistentSearch.prototype.fromString = function(serialized) {
var properties = serialized.split("&");
var search = new PersistentSearch("", "");
for (var i=0; i < properties.length; i++) {
var keyValue = properties[i].split("=");
search[keyValue[0]] = keyValue[1];
}
return search;
}
PersistentSearch.prototype.getListItem = function() {
if (!this.listItem) {
this.listItem = newNode("div");
this.listItem.className = "lk cs listItem";
this.listItem.appendChild(newText(this.label));
this.resultSizeItem = newNode("span");
this.listItem.appendChild(this.resultSizeItem);
this.getResultSize(false);
this.listItem.onclick = getObjectMethodClosure(this, "execute");
}
return this.listItem;
}
PersistentSearch.prototype.getEditItem = function() {
if (!this.editItem) {
this.editItem = newNode("tr");
this.editItem.className = "editItem";
var labelCell = newNode("td");
labelCell.className = "labelCell";
var labelInput = newNode("input");
labelInput.value = this.label;
labelCell.appendChild(labelInput);
this.editItem.appendChild(labelCell);
var editCell = newNode("td");
editCell.className = "editCell";
var queryInput = newNode("input");
queryInput.value = this.getEditableQuery();
editCell.appendChild(queryInput);
this.editItem.appendChild(editCell);
}
return this.editItem;
}
// Monitor item link
PersistentSearch.prototype.execute = function() {
var queryUrl = "/scholar?hl=en&q=" + escape(this.query);
window.location = "http://scholar.google.com" + queryUrl;
}
PersistentSearch.prototype.getRunnableQuery = function() {
var query = this.query;
var today = new Date();
query = query.replace(/:thisyear/g, ":" + today.getYear());
return query;
}
PersistentSearch.prototype.getEditableQuery = function() {
return this.query;
}
PersistentSearch.prototype.getResultSize = function(needsRefresh) {
// Refresh data if it's a new item or older than a day
if (this.totalResults == -1 || this.oldResults == -1 ||
this.setDate < new Date()- ONE_DAY) {
needsRefresh = true;
} else {
this.updateResultSizeItem();
}
if (needsRefresh) {
this.resultSizeItem.style.display = "none";
this.runQuery(this.getRunnableQuery(),
getObjectMethodClosure1(this, "updateResultSize"));
}
}
PersistentSearch.prototype.runQuery = function(query, continuationFunction) {
var queryUrl = "/scholar?hl=en&q=" + escape(query);
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == 4 && request.status == 200) {
var match = RESULT_SIZE_RE.exec(request.responseText);
if (match) {
var resultSize = match[3];
continuationFunction(resultSize);
} else {
continuationFunction(-1);
}
}
}
request.open("GET", queryUrl, true);
request.send(null);
}
PersistentSearch.prototype.updateResultSize = function(totalResults) {
this.totalResults = totalResults;
// Update Resuls. It will set oldResult and setNewDate
this.updateResultSizeItem();
// Save Result
savePersistentSearches();
}
PersistentSearch.prototype.updateResultSizeItem = function() {
if (this.resultSizeItem) {
// Clear existing contents
var child;
this.resultSizeItem.style.display = "";
while (child = this.resultSizeItem.firstChild) {
this.resultSizeItem.removeChild(child);
}
// Update with new values
this.resultSizeItem.appendChild(newText(" ("));
// oldResults is used for old Count
// If oldCount is different from new onw, bold
var today = new Date().getTime();
if (this.oldResults != this.totalResults) {
this.newSetDate = today;
// Syncronize
this.oldResults = this.totalResults;
}
// Set date
this.setDate = today;
var isNew = this.newSetDate > today - ONE_HOUR;
var resultCount;
if (isNew) {
resultCount = newNode("b");
} else {
resultCount = newNode("span");
}
resultCount.appendChild(newText(this.totalResults));
this.resultSizeItem.appendChild(resultCount);
this.resultSizeItem.appendChild(newText(")"));
if (isNew) {
var newMark = newNode("sup");
newMark.innerHTML = "<font color=red>New!</font>"
this.resultSizeItem.appendChild(newMark);
}
}
}
function initializeStyles() {
var styleNode = newNode("style");
document.body.appendChild(styleNode);
styleSheet = document.styleSheets[document.styleSheets.length - 1];
for (var i=0; i < RULES.length; i++) {
styleSheet.insertRule(RULES[i], 0);
}
styleSheet.insertRule(NORMAL_RULE, MESSAGE_BODY_FONT_RULE_INDEX);
}
initializeStyles();
initializePersistentSearches();
})();