Created
February 15, 2012 15:11
-
-
Save ChrisMoney/1836534 to your computer and use it in GitHub Desktop.
Javascript -Focus on the user script that returns google results without google+ data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (function(){ | |
| if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) { | |
| alert( | |
| "Unfortunately, this tool does not currently work in Internet Explorer. " + | |
| "Please try another browser such as Firefox, Chrome, or Safari."); | |
| return; | |
| } | |
| var anchor = document.getElementsByTagName('script')[0]; | |
| if (!anchor) { return; } | |
| var link = document.createElement('link'); | |
| link.rel = 'stylesheet'; | |
| link.type = 'text/css'; | |
| link.href = '//focusontheuser.org/dontbeevil/style.css?3'; | |
| anchor.parentNode.insertBefore(link, anchor); | |
| var script = document.createElement('script'); | |
| script.src = '//ajax.googleapis.com/ajax/libs/mootools/1.4.1/mootools-yui-compressed.js'; | |
| script.onload = main; | |
| anchor.parentNode.insertBefore(script, anchor); | |
| // Main Application | |
| function main() { | |
| // A custom pseudo selector for finding highlighted typeahead entries. | |
| Slick.definePseudo('highlighted', function() { | |
| return this.getStyle('background-color') != 'transparent'; | |
| }); | |
| // Debounce to prevent updatePage from being called too often. | |
| var debouncedUpdatePage = debounce(updatePage); | |
| // The main results div will serve as our flag. Store a reference to this | |
| // node, and get it by id in a loop. Update the page whenever it changes. | |
| var getFlag = function() { return $('rso'); }; | |
| var oldFlag = getFlag(); | |
| var compare = function() { | |
| var newFlag = getFlag(); | |
| if (newFlag != oldFlag) { | |
| oldFlag = newFlag; | |
| debouncedUpdatePage(); | |
| } | |
| }; | |
| setInterval(compare, 100); | |
| setupTypeahead(); | |
| updatePage(); | |
| } | |
| function updatePage() { | |
| updateTypeahead(); | |
| updateRelatedBox(); | |
| updateResultStats(); | |
| updateContentLinks(); | |
| updateRelatedLinks(); | |
| } | |
| // When the user searchs for a Twitter username including the leading @, | |
| // redirect them directly to Twitter if their search shows up on the page. | |
| function redirectIfTwitterUsername() { | |
| var input = $$('input[name=q]')[0]; | |
| if (input && (/^@[A-Za-z0-9_]+$/).test(input.value)) { | |
| var username = input.get('value').substr(1).toLowerCase(); | |
| $$('li.g div.s div.f cite').some(function(item) { | |
| var path = item.get('text').toLowerCase(); | |
| if (path == ("twitter.com/" + username) || | |
| path == ("twitter.com/#!/" + username) || | |
| path == ("twitter.com/#!/@" + username)) { | |
| document.location = "https://twitter.com/" + username; | |
| } | |
| }); | |
| } | |
| } | |
| // Attach some event listeners to the main text input, so we can navigate when | |
| // the user uses enter to select a result from the typeahead. | |
| function setupTypeahead() { | |
| var href; | |
| var input = $('gbqfq') || $$('input[name=q]')[0]; | |
| if (input) { | |
| input.addEvents({ | |
| keyup: function() { | |
| var span = $$('body > table tr:highlighted span:contains(".com")')[0]; | |
| href = span && span.retrieve('href') || ''; | |
| }, | |
| keydown: function(event) { | |
| if (event.key != 'enter') { return; } | |
| if (href) { | |
| window.open(href); | |
| event.stop(); | |
| } else { | |
| redirectIfTwitterUsername(); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| // Look for any typeahead entries that contain '.com' in a span, since this is | |
| // one differentiating feature of the Google+ entries. For each result, perform | |
| // a full search for the text of that entry, and 'replace' the Google+ results | |
| // with the highest ranked result as returned by the full search. | |
| function updateTypeahead() { | |
| var spans = $$('body > table tr span:contains(".com")'); | |
| spans.each(function(span) { | |
| var row = span.getParent('tr'); | |
| var cell = row.getLast('td'); | |
| var span = cell.getLast('span'); | |
| // Some entries might contain .com, but not be the double height G+ entries. | |
| if (!span) { return; } | |
| var name = cell.getFirst('span').get('text'); | |
| getSearchResult(name, function(result) { | |
| if (!result) { return; } | |
| var img = row.getElement('img'); | |
| var item = row.getParent('tr'); | |
| // Attach a click listener, which will be used to navigate on select, to | |
| // each entry in the typeahead. Results are reused, so only attach once. | |
| if (item && !item.retrieve('attached')) { | |
| item.store('attached', true); | |
| item.addEvent('click', function(event) { | |
| window.open(span.retrieve('href')); | |
| }); | |
| } | |
| // If we have a photo for this item, update the typeahead entry, otherwise | |
| // memoize the existing photo in our result object for later use. | |
| result.photo ? (img.src = result.photo) : (result.photo = img.src); | |
| // Update the site that's displayed. | |
| span.set('text', ' \xb7 ' + result.site); | |
| // Store a reference to the href needed for navigation (used with 'enter'). | |
| span.store('href', result.href); | |
| }); | |
| }); | |
| } | |
| // This tweaks the text in the right hand column to say "People and Pages from | |
| // the Social Web", rather than "People and Pages on Google+". The column gets | |
| // completely replaced on every search, so we need to run this every time. | |
| function updateRelatedBox() { | |
| var fieldset = $('aerhs'); | |
| if (fieldset) { | |
| var legend = fieldset.getFirst('legend'); | |
| legend && legend.set('text', 'People and Pages from the Social Web'); | |
| } | |
| } | |
| // Show a little blurb with a link in the result stats area. This also needs to | |
| // be updated along with every query. | |
| function updateResultStats() { | |
| var stats = $$('#appbar :contains("seconds")').pop(); | |
| if (stats) { | |
| // Probably only need to do this once, but it doesn't hurt. | |
| stats.setStyle('overflow', 'visible'); | |
| stats.getParent('div').setStyle('overflow', 'visible'); | |
| stats.adopt([ | |
| new Element('span').adopt([ | |
| document.createTextNode('\xb7 '), | |
| new Element('span', {'class': 'dbe_stats'}).adopt([ | |
| document.createTextNode('results improved by '), | |
| new Element('a', { | |
| href: 'http://www.focusontheuser.org', | |
| text: 'FocusOnTheUser.org' | |
| }) | |
| ]) | |
| ]) | |
| ]); | |
| } | |
| } | |
| // Find the special Google+ links attached directly to search results in the | |
| // middle column, and replace them with the most relevant social result. | |
| function updateContentLinks() { | |
| var link = $$('#rso a[href*="plus.google.com"].l')[0]; | |
| if (!link) { return; } | |
| var text = link.get('text').replace(' - Google+', ''); | |
| if (!text) { return; } | |
| var item = link.getParent('li.g'); | |
| if (!item) { return; } | |
| getSearchResult(text, function(result) { | |
| if (!result) { return; } | |
| // Memoize the node, and apply the style from the item we're replacing. | |
| if (!result.plusNode) { | |
| result.plusNode = result.node.set('style', item.get('style')).adopt(result.links); | |
| } | |
| // Plus links are indented.. this seems to be a pretty simple way to make | |
| // sure we only replace the correct links. | |
| if (parseInt(item.getStyle('margin-left'), 10) > 0) { | |
| // Oh, why not use a little superfluous animation. | |
| item.get('morph').start({opacity: [1, 0]}).chain(function() { | |
| var morph = result.plusNode.get('morph').set({opacity: 0}); | |
| result.plusNode.replaces(item).highlight('#fea'); | |
| morph.start({opacity: [0, 1]}); | |
| }); | |
| } | |
| }); | |
| } | |
| // Find the Google+ links in the right hand column and replace them with the | |
| // most relevant social results, including all links to social sites that show | |
| // up in the top 100 results, ranked by Google. | |
| function updateRelatedLinks() { | |
| var links = $$('#rhs_block a[href*="profiles.google.com"]'); | |
| links.each(function(link) { | |
| var text = (link.get('text') || '').replace(/\.com$/, ''); | |
| if (!text) { return; } | |
| var item = link.getParent('table'); | |
| if (!item) { return; } | |
| getSearchResult(text, function(result) { | |
| if (!result) { return; } | |
| if (!result.profileBlock) { | |
| result.profileBlock = (result.site != 'plus.google.com') ? | |
| buildProfileBlockMarkup(result, item.getElement('img').get('src')) : | |
| item.clone(); | |
| } | |
| item.get('morph').start({opacity: [1, 0]}).chain(function() { | |
| var div = new Element('div', {'class': 'dbe_results'}); | |
| var morph = div.get('morph').set({opacity: 0}); | |
| div.adopt([result.profileBlock, result.links]).replaces(item).highlight('#fea'); | |
| morph.start({opacity: [0, 1]}); | |
| }); | |
| }) | |
| }); | |
| } | |
| // For the most part, we're just going to use the markup as returned to us by | |
| // Google's servers, and inject it into the various places on the page. We do | |
| // want to modify it a little bit, adding photos, like / tweet buttons, etc, | |
| // and then we'll memoize the node for later use. | |
| function buildProfileBlockMarkup(result, defaultPhoto) { | |
| var node = result.node; | |
| var site = result.site; | |
| var text = node.getElement('h3'); | |
| var link = text.getElement('a'); | |
| var href = link.get('href'); | |
| link.set('title', link.get('text')).set('text', result.text); | |
| // Remove the 'Person Shared This' box. | |
| var div = text.getNext().getNext(); | |
| div && div.dispose(); | |
| var src = '', html = '', span; | |
| if (site == 'facebook.com') { | |
| // The Facebook like button is dumb, and even shows up for profiles, | |
| // where subscribe should be the default action - override for zuck. | |
| var buttonType = href.indexOf('/zuck') > 0 ? 'subscribe' : 'like'; | |
| src = '//connect.facebook.net/en_US/all.js#xfbml=1'; | |
| html = '<div href="' + href + '" class="fb-' + buttonType + '" data-show-faces="false" data-layout="button_count" data-width="120" data-send="false"></div>'; | |
| } else if (site == 'twitter.com') { | |
| src = '//platform.twitter.com/widgets.js'; | |
| html = '<a href="' + href + '" class="twitter-follow-button" data-show-screen-name="false"></a>'; | |
| // Remove the repetitive twitter default sign up text. | |
| span = node.getElement('span:contains("Sign up for Twitter")'); | |
| span && span.set('text', span.get('text').replace(/(\s+|^)Sign up for Twitter[\s\w\-]+\(@\w+\)\.\s+/, '')); | |
| } else if (site == 'linkedin.com') { | |
| span = node.getElement('span:contains("professional profile on LinkedIn")'); | |
| if (span) { | |
| var blurb = span.getPrevious(); | |
| span.set('text', blurb.get('text')); | |
| blurb.dispose(); | |
| } | |
| } | |
| if (src) { | |
| var script = new Element('script', {src: src, onload: function() { script.dispose(); }}).inject(anchor, 'top'); | |
| new Element('div', {'class': 'dbe_button', html: html}).inject(text, 'bottom'); | |
| } | |
| new Element('a', {href: href}) | |
| .adopt(new Element('img', {src: result.photo || defaultPhoto, 'class': 'dbe_img'})) | |
| .inject(text.getNext(), 'top'); | |
| return new Element('ul', {'class': 'dbe_first'}).adopt(node); | |
| } | |
| // This function builds the little div of · separated links that shows | |
| // up below the most relevant search result in the middle and right columns. | |
| function buildLinksDiv(results) { | |
| var nodes = []; | |
| results.each(function(result, index) { | |
| nodes.push(new Element('a', { | |
| href: result.href, | |
| text: siteMap[result.site].name, | |
| title: link.get('text') | |
| })); | |
| if (index < results.length - 1) { | |
| nodes.push(document.createTextNode(' \xb7 ')); | |
| } | |
| }); | |
| return new Element('div', {'class': 'dbe_extra'}).adopt(nodes); | |
| } | |
| // Functions for fetching data | |
| // Given a query string and a callback, this function will perform an | |
| // asynchronous Google search for the top 100 results, parse the response into | |
| // HTMLLIElement DOM nodes, filter that set down to only meaningful socially | |
| // relevent results, fetch photos for the linked profiles of those results, | |
| // and invoke the callback provided with a sorted list of result objects. | |
| var getSearchResult = (function() { | |
| // Figure out where the search result passed in points to, by looking at the | |
| // first anchor element inside the node. | |
| var getHrefFromNode = function(node) { | |
| var link = $(node).getElement('a'); | |
| return link ? link.get('href') : ''; | |
| } | |
| // Retrieve a photo for the provided result object, based either on a | |
| // specially crafted Google image query (for Twitter), or on a query to | |
| // Google's own richsnippets web tool. | |
| var getPhoto = function(result, callback) { | |
| if (!siteMap[result.site].photo) { | |
| return callback(); | |
| } | |
| var href = result.href; | |
| var url = '//www.google.com/'; | |
| if (result.site == 'twitter.com') { | |
| // Get Twitter profile pic by google image searching for the twitter | |
| // profile's url, e.g. "site:twitter.com/britneyspears", and then | |
| // extracting the first result. This seems to usually get the right thing, | |
| // though often the wrong size. | |
| url += 'search?gbv=2&tbm=isch&fp=1&biw=1&bih=1&tch=1&q=' + encodeURIComponent('site:' + href); | |
| xhr(url, function(text) { | |
| var matches = unescape(text).match(/imgurl\\=(https?:\/\/[^.]+\.twimg\.com\/profile_images\/\d+\/[^\\&]+)/); | |
| var match = matches ? matches[1].replace(/_(?:bigger|mini|normal|reasonably_small)/, '').replace(/\.jpg$/, '_normal.jpg') : ''; | |
| callback(match); | |
| }); | |
| } else { | |
| url += 'webmasters/tools/richsnippets?view=cse&url=' + encodeURIComponent(href); | |
| xhr(url, function(text) { | |
| var matches = text.match(/(?:image|photo)\s*=\s*(.+?)\s*<br\s*\/>/m); | |
| var match = matches ? matches[1] : ''; | |
| callback(match); | |
| }); | |
| } | |
| }; | |
| // Out of all the results in the result set, find the socially meaningful | |
| // ones, discarding all the rest. This function is used to return a sorted | |
| // list of results for each query, and should aggressively filter out | |
| // unrelated results. | |
| var findMeaningfulResults = function(text, results) { | |
| var hrefs = []; | |
| var sources = []; | |
| var matched = {}; | |
| return results.filter(function(result) { | |
| // Filter out any results without an href. | |
| var href = getHrefFromNode(result); | |
| if (!href) { return false; } | |
| // Filter down to only the sites we care about. | |
| var matches = href.match(siteMatcher); | |
| if (!matches) { return false; } | |
| // Make sure we haven't already found a higher ranked site match. | |
| var site = matches[1]; | |
| var alreadyMatched = matched[site]; | |
| if (alreadyMatched) { return false; } | |
| // If we have a whitelist for this site, make sure it matches. | |
| var whitelist = siteMap[site].whitelist; | |
| if (whitelist && !whitelist.test(href)) { return false; } | |
| // If we have a blacklist for this site, make sure it doesn't match. | |
| var blacklist = siteMap[site].blacklist; | |
| if (blacklist && blacklist.test(href)) { return false; } | |
| // Store at most one match per site, including site name and href. | |
| hrefs.push(href); | |
| sources.push(site); | |
| return (matched[site] = true); | |
| }).map(function(node, index) { | |
| return { | |
| node: node, | |
| text: text, | |
| href: hrefs[index], | |
| site: sources[index] | |
| } | |
| }); | |
| }; | |
| // This crazy function will parse a Google XHR response, pulling it apart | |
| // into chunks, and turning each of those individual chunks into a DOM node | |
| // that can then be analyzed / inserted into the DOM. | |
| var parseResponseText = function(text) { | |
| var offset = 0; | |
| var openTag = "\\x3c!--m--\\x3e"; | |
| var closeTag = "\\x3c!--n--\\x3e"; | |
| var tagLength = 14; | |
| var getNextResult = function() { | |
| var result = null; | |
| var open = text.indexOf(openTag, offset); | |
| var close = text.indexOf(closeTag, offset); | |
| var nextOpen = text.indexOf(openTag, open + tagLength); | |
| var nextClose = text.indexOf(closeTag, close + tagLength); | |
| if (nextOpen < close) { | |
| close = nextOpen; | |
| offset = nextClose + tagLength; | |
| } else { | |
| offset = close + tagLength; | |
| } | |
| if (open >= 0 && close >= 0) { | |
| var html = unescape(text.substring(open + tagLength, close)); | |
| result = new Element('ul', {html: html}).getFirst(); | |
| } | |
| return result; | |
| }; | |
| var results = []; | |
| var nextResult = null; | |
| while (nextResult = getNextResult()) { | |
| results.push(nextResult); | |
| } | |
| return results; | |
| }; | |
| var cache = {}; | |
| return function(query, callback) { | |
| if (query in cache) { | |
| return callback(cache[query]); | |
| } | |
| var url = '//www.google.com/search?q=' + encodeURIComponent(query) + '&xhr=t&fp=1&psi=1&num=100&complete=0&pws=0'; | |
| xhr(url, function(text) { | |
| var results = findMeaningfulResults(query, parseResponseText(text)); | |
| var result = results[0]; | |
| if (result) { | |
| result.links = buildLinksDiv(results.slice(1)); | |
| getPhoto(result, function(photo) { | |
| result.photo = photo || ''; | |
| cache[query] = result; | |
| callback(result); | |
| }); | |
| } | |
| }); | |
| }; | |
| })(); | |
| // Generic Utilities | |
| function debounce(func) { | |
| var timer = null; | |
| return function() { | |
| clearTimeout(timer); | |
| timer = setTimeout(func, 500); | |
| } | |
| } | |
| var unescape = (function() { | |
| var regex = /\\x22|\\x26|\\x27|\\x3c|\\x3d|\\x3e/g; | |
| var callback = function(match) { | |
| switch (match) { | |
| case "\\x22": return '"'; | |
| case "\\x26": return "&"; | |
| case "\\x27": return "'"; | |
| case "\\x3c": return "<"; | |
| case "\\x3d": return "="; | |
| case "\\x3e": return ">"; | |
| } | |
| }; | |
| return function(str) { | |
| return str.replace(regex, callback); | |
| }; | |
| })(); | |
| function xhr(url, callback) { | |
| var req = google.xhr(); | |
| req.open("GET", url, true); | |
| req.onreadystatechange = function(evt) { | |
| if (req.readyState === 4) { | |
| callback(req.responseText); | |
| } | |
| }; | |
| req.send(null); | |
| } | |
| // Sites | |
| var siteMap = { | |
| 'crunchbase.com': { | |
| name: 'CrunchBase', | |
| whitelist: /(?:^|[\/.])crunchbase\.com\/(person|company)\/(\w+)\/?$/i | |
| }, | |
| 'facebook.com': { | |
| name: 'Facebook', | |
| photo: true, | |
| whitelist: /(?:^|[\/.])facebook\.com\/([a-zA-Z0-9.]+)\/?$/i | |
| }, | |
| 'flickr.com': { | |
| name: 'Flickr', | |
| whitelist: /(?:^|[\/.])flickr\.com\/people\/(\w+)\/?$/i | |
| }, | |
| 'foursquare.com': { | |
| name: 'Foursquare', | |
| whitelist: /(?:^|[\/.])foursquare\.com\/(\w+)\/?$/i | |
| }, | |
| 'friendfeed.com': { | |
| name: 'FriendFeed', | |
| whitelist: /(?:^|[\/.])friendfeed\.com\/(\w+)\/?$/i | |
| }, | |
| 'github.com': { | |
| name: 'GitHub', | |
| whitelist: /(?:^|[\/.])github\.com\/(\w+)\/?$/i | |
| }, | |
| 'linkedin.com': { | |
| name: 'LinkedIn', | |
| photo: true, | |
| whitelist: /(?:^|[\/.])linkedin\.com\/in\/(\w+)\/?$/i | |
| }, | |
| 'myspace.com': { | |
| name: 'MySpace', | |
| photo: true, | |
| whitelist: /(?:^|[\/.])myspace\.com\/(\w+)\/?$/i | |
| }, | |
| 'plus.google.com': { | |
| name: 'Google+', | |
| whitelist: /(?:^|[\/.])plus\.google\.com\/(\w+)\/?$/i | |
| }, | |
| 'quora.com': { | |
| name: 'Quora', | |
| photo: true, | |
| whitelist: /(?:^|[\/.])quora\.com\/([\w\-]+)\/?$/i, | |
| blacklist: /\/(who|what|when|where|why|how)\-/i | |
| }, | |
| 'stackoverflow.com': { | |
| name: 'Stack Overflow', | |
| whitelist: /(?:^|[\/.])stackoverflow\.com\/users\/(\w+)\/?/i | |
| }, | |
| 'tumblr.com': { | |
| name: 'Tumblr', | |
| whitelist: /(?:^|[\/.])(\w+)\.tumblr\.com\/?$/, | |
| blacklist: /\/tagged\// | |
| }, | |
| 'twitter.com': { | |
| photo: true, | |
| name: 'Twitter', | |
| whitelist: /(?:^|[\/.])twitter\.com\/(\w+)\/?$/i, | |
| blacklist: /\/statuses\// | |
| }, | |
| 'youtube.com': { | |
| name: 'YouTube', | |
| whitelist: /(?:^|[\/.])youtube\.com\/user\/(\w+)(?:\/featured)?\/?$/ | |
| } | |
| }; | |
| var siteMatcher = (function(){ | |
| var sites = []; | |
| for (var site in siteMap) { sites.push(site); } | |
| return new RegExp('[\/.](' + sites.join('|').replace('.', '\\.') + ')'); | |
| })(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment